Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Walkthrough프론트엔드: Next.js 앱 초기 구조 및 전역 스타일/유틸 추가, 다수의 UI 컴포넌트(레イ아웃, 사이드바, 헤더, 다이얼로그, 폴더/링크 카드 등), Zustand 상태 저장소, React Query 기반 API 클라이언트와 타입 정의, 북마크/폴더/태그 관련 훅 및 fetchClient 구현을 추가합니다. 백엔드: Bookmark API에 URL 분석 엔드포인트(GET /api/bookmarks/analyze) 추가, URL 정규화(canonical_url)로 링크 조회 전환, Jsoup 및 YouTube oEmbed 사용한 제목 추출 로직 추가. 문서: 백엔드/프론트엔드 규칙, PRD, KPI 설계 등 문서 추가. Sequence Diagram(s)sequenceDiagram
actor User
participant Frontend as SaveLinkDialog
participant FetchClient as Frontend\ API\ Client
participant Backend as BookmarkApiController
participant Service as BookmarkService
participant Jsoup as Jsoup/YouTube\ APIs
User->>Frontend: 저장 대화상자 열고 URL 입력
Frontend->>FetchClient: analyzeUrl(url) 호출
FetchClient->>Backend: GET /api/bookmarks/analyze?url=...
Backend->>Service: extractTitle(url)
alt YouTube URL
Service->>Jsoup: call YouTube\ oEmbed\ API
Jsoup->>Service: oEmbed\ title
else 일반 웹페이지
Service->>Jsoup: fetch\ +\ parse\ HTML
Jsoup->>Service: og:title|twitter:title|<title>
end
Service->>Backend: 반환된\ 제목
Backend->>FetchClient: ApiResponse<String>(title)
FetchClient->>Frontend: 제목 표시
User->>Frontend: 제목/폴더/태그 선택 후 저장
Frontend->>FetchClient: createBookmark(data)
FetchClient->>Backend: POST /api/bookmarks
Backend->>Service: 북마크 생성(정규화 URL 사용)
Backend->>FetchClient: 생성 성공 응답
FetchClient->>Frontend: 업데이트 완료
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2cfd75f5e5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| {/* Material Symbols Outlined Font */} | ||
| <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/> | ||
| </head> | ||
| <body className={`${inter.variable} font-sans antialiased bg-background-light dark:bg-background-dark text-text-main h-screen overflow-hidden flex transition-colors duration-200`}> |
There was a problem hiding this comment.
Remove global scroll lock from public pages
RootLayout applies h-screen overflow-hidden to the <body>, and AppLayout bypasses its internal scroll container for / and /login, so those pages have no vertical scrolling path. As a result, below-the-fold content (for example the landing page sections like #features/#pricing, and lower login content on shorter viewports) becomes inaccessible.
Useful? React with 👍 / 👎.
| { name: 'History', href: '/history', icon: 'history' }, | ||
| { name: 'Tags', href: '/tags', icon: 'tag' }, | ||
| { name: 'Settings', href: '/settings', icon: 'settings' }, |
There was a problem hiding this comment.
Remove or implement sidebar links that always 404
The sidebar advertises /history, /tags, and /settings, but this change only defines pages for /, /login, and /my-links in frontend/src/app. Navigating to these menu items therefore lands users on a 404 screen, breaking core in-app navigation until those routes exist.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46e4590e6a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return [ | ||
| { | ||
| source: '/api/:path*', | ||
| destination: 'http://localhost:8080/api/:path*', |
There was a problem hiding this comment.
Parameterize API rewrite destination
Rewriting every /api/* request to http://localhost:8080 hard-codes a local-only backend target, so any non-local environment (staging, production, preview, or Docker setups where backend is not same-container localhost) will proxy useFolders calls to an unreachable host and fail folder loading. Making this destination environment-driven avoids shipping a config that breaks API access outside local dev.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: da174ebd19
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (currentUser == null || "anonymousUser".equals(currentUser)) { | ||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); | ||
| } | ||
|
|
||
| Long memberId = getMemberId(currentUser); | ||
| if (memberId == null) { | ||
| return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); | ||
| memberId = 1L; |
There was a problem hiding this comment.
Reinstate authentication checks for bookmark APIs
This change maps anonymous requests to a hard-coded memberId = 1L, which turns unauthenticated traffic into full access to one real account instead of returning 401; the same pattern is repeated across create/read/list/update/delete/check handlers in this controller. That is an authorization bypass and allows anyone to read or mutate member 1's bookmarks without logging in.
Useful? React with 👍 / 👎.
| updateBookmarkMutation.mutate({ | ||
| bookmarkId, | ||
| note, | ||
| memberFolderId: bookmark.memberFolderId, | ||
| displayTitle: bookmark.displayTitle, |
There was a problem hiding this comment.
Preserve primary category when sending bookmark updates
This update payload omits primaryCategoryId, but the backend update SQL unconditionally writes primary_category_id = #{primaryCategoryId} (src/main/resources/mapper/bookmark-mapper.xml, updateBookmark), so note/title/folder/tag edits from this panel will clear existing categories to NULL. That silently corrupts bookmark classification data during normal inline edits.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| try { | ||
| Document doc = Jsoup.connect(url) |
There was a problem hiding this comment.
Validate analyze URLs before server-side fetch
The new title extraction path fetches user-controlled URLs directly via Jsoup.connect(url).get() with no network or host restrictions, which enables SSRF if /api/bookmarks/analyze is reachable (for example, probing internal HTTP services or cloud metadata endpoints). The handler should reject internal/private targets and non-HTTP(S) schemes before making outbound requests.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 49
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java (2)
187-208:⚠️ Potential issue | 🟠 Major
getOrCreateLink에 동시성 레이스가 있어 중복 삽입 예외로 터질 수 있습니다.Line [192]~[208]은 “조회 후 삽입” 패턴이라 동시 요청에서 둘 다
null을 보고 동시에 insert를 시도할 수 있습니다. 이 경우DataIntegrityViolationException이 발생하고 현재 메서드에서 재조회 복구를 하지 않습니다.🔧 제안 수정안
Link existingLink = bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl); if (existingLink != null) { return existingLink; } Link newLink = Link.builder() .canonicalUrl(canonicalUrl) .originalUrl(url) .domain(extractDomain(url)) .title(url) .primaryCategoryId(1L) .createdByMemberId(createdByMemberId) .build(); - bookmarkDao.insertLink(newLink); - return newLink; + try { + bookmarkDao.insertLink(newLink); + return newLink; + } catch (DataIntegrityViolationException e) { + Link concurrentLink = bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl); + if (concurrentLink != null) return concurrentLink; + throw e; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java` around lines 187 - 208, getOrCreateLink has a race between selectLinkByCanonicalUrl and insertLink that can cause duplicate-insert exceptions; wrap the insert (bookmarkDao.insertLink(newLink)) in a try/catch for the database uniqueness exception (e.g., DataIntegrityViolationException or the specific duplicate-key exception used) and on catch re-query bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl) and return that existing Link; keep the original successful return path for the newly inserted Link and ensure canonicalUrl/newLink/insertLink/selectLinkByCanonicalUrl are the referenced symbols to locate the change.
217-224:⚠️ Potential issue | 🔴 CriticalcheckBookmarkExistsByUrl 쿼리가 mapper XML에 정의되지 않았습니다.
Line 219에서
canonicalUrl로 Link를 조회한 후 Line 224에서checkBookmarkExistsByUrl(memberId, url)을 호출하지만,src/main/resources/mapper/bookmark-mapper.xml에 이 쿼리가 정의되지 않아 런타임에 MyBatis 예외가 발생합니다.북마크 존재 여부를 확인하는 SQL을 mapper XML에 추가해야 합니다. 추가 시 다음을 확인하세요:
- Line 219에서 이미 찾은 Link의 linkId를 재사용하거나 canonicalUrl 기반 조회로 일관성 있게 구현할 것
- 현재 코드는 canonicalUrl로 Link를 찾은 뒤 raw url 파라미터로 다시 체크하고 있어, URL 변형(트래킹 파라미터/슬래시 차이)으로 인한 불일치 위험이 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java` around lines 217 - 224, BookmarkServiceImpl currently normalizes the URL via normalizeUrl and fetches Link with bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl) but then calls bookmarkDao.checkBookmarkExistsByUrl(memberId, url) which has no MyBatis mapping and risks mismatch; fix by adding a proper SQL mapper in src/main/resources/mapper/bookmark-mapper.xml and change the service to reuse the found Link (e.g., call a new checkBookmarkExistsByLinkId(memberId, linkId) mapped in bookmark-mapper.xml) or alternatively add a checkBookmarkExistsByCanonicalUrl(memberId, canonicalUrl) SQL mapping and call that from BookmarkServiceImpl so the lookup is consistent with selectLinkByCanonicalUrl.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/Backend/01`. backend-convention.md:
- Line 5: The markdown headers (e.g., "## 1) 네이밍 규칙") violate markdownlint MD022
because they lack a blank line before and/or after the header; add a single
empty line above and a single empty line below each header occurrence (including
the headers at the specified locations such as the "## 1) 네이밍 규칙" header and the
other headers referenced) so every header has a blank line separating it from
surrounding content to resolve the lint warnings.
In `@docs/Fontend/01`. frontend_architecture.md:
- Line 42: Headings in the document (e.g., "### 1. 프로젝트 기반 및 환경 설계" and the
other headings referenced at lines 47, 52, 87, 92, 97, 102, 108) violate MD022;
ensure each heading is surrounded by a single blank line above and below it
(i.e., insert an empty line before the heading if missing and an empty line
after the heading), apply this consistently for the listed headings so
markdownlint no longer emits the MD022 warning.
In `@docs/Plan/01`. searchweb_PRD.md:
- Around line 71-72: Remove extra blank lines and stray spaces immediately
before and after Markdown headers and fenced code blocks to satisfy markdownlint
rules MD022 and MD031; specifically edit the sections containing the bullet
lines "* 저장된 링크 중 AI 태그 포함 비율 **60% 이상**" and "* AI 추천 태그 채택률 **50% 이상**" (and
the repeated occurrences at the other noted ranges) so there is exactly one
blank line separating headers/fences from surrounding content and no trailing or
leading spaces adjacent to header/fence markers.
In `@docs/Plan/02`. kpi_measurement_design.md:
- Line 53: PAGE_VIEW is defined as an anonymous Frontend event but the events
table has a member_id NOT NULL constraint, which prevents storing anonymous
events; either make member_id nullable in the schema (remove or change the NOT
NULL constraint on member_id in the events table) so PAGE_VIEW can be saved
without a user, or change the PAGE_VIEW definition to require authenticated
users and ensure Frontend supplies member_id for that event; update the schema
constraint and/or the PAGE_VIEW event definition (and any ingestion validation)
so both definitions are consistent.
- Line 6: Replace the absolute file:// link on the line containing the markdown
link text "searchweb_PRD.md" with a repository-relative path so the link works
in CI/review environments; change `file:///e:/.../searchweb_PRD.md` to a
relative link like `../Plan/searchweb_PRD.md` (or the correct relative path from
docs/Plan/02. kpi_measurement_design.md to the searchweb_PRD.md file) and ensure
the markdown link syntax remains valid.
In `@frontend/next.config.ts`:
- Around line 5-11: The rewrite currently hardcodes destination
'http://localhost:8080' in async rewrites(), which breaks in non-dev
environments; change rewrites() to read the backend base URL from an environment
variable (e.g. process.env.BACKEND_URL) and, if that env var is unset or empty,
return an empty array to disable the rewrite; when set, build the rule using
source '/api/:path*' and destination `${backendUrl}/api/:path*` (ensure
backendUrl includes protocol) so the rewrite works per-environment.
In `@frontend/README.md`:
- Line 1: Add a top-level H1 header as the very first line to satisfy
markdownlint MD041 (e.g., insert a line starting with “# ” above the current
Line 1 content in README.md) so the document begins with a single H1 heading
before the existing paragraph.
- Around line 19-21: The README's template lines reference the wrong entry path
and font; update the sentence that mentions `app/page.tsx` to point to the
actual entry `src/app/page.tsx` (or the project's current app entry) and replace
the `next/font`/Geist description with the actual font setup used in this repo
(Inter via the project's configured method, e.g., `next/font` import of Inter or
local font configuration), ensuring the wording matches the repo's `src/app/*`
structure and the imported font name `Inter` so newcomers won't be misled.
In `@frontend/src/app/globals.css`:
- Around line 125-141: There are two separate `@theme` inline blocks (one
containing variables like --color-primary, --color-background-dark,
--color-text-main, etc. and the other shown here) — merge them into a single
`@theme` inline block that contains the union of all CSS custom properties, remove
the duplicate block, dedupe any repeated variables (keep the intended value),
and preserve ordering/comments so all theme variables (e.g., --color-primary,
--color-primary-dark, --color-background-light, --color-sidebar-dark,
--color-text-secondary-dark) are maintained in one place.
In `@frontend/src/app/layout.tsx`:
- Around line 28-31: Replace the inline Google Fonts <link> in the <head> of the
RootLayout component (the <head> block in frontend/src/app/layout.tsx) with
Next.js font handling: remove the external <link> and either (a) configure the
Material Symbols via next/font (or the new metadata/font entry) to leverage
automatic optimization and self-hosting, or (b) download the variable Material
Symbols font files, add them to /public or import them as local fonts with
next/font/local, then reference that local font in RootLayout so the font is
loaded and applied via the exported font object rather than an external <link>;
if you must keep the Google-hosted variable font, add a clear comment explaining
the variable-font rationale and ensure preconnect or font-display strategies are
configured in metadata.
In `@frontend/src/app/login/page.tsx`:
- Around line 28-30: Two buttons in the login page component (the mobile menu
button with className "md:hidden text-white" containing the "menu" material icon
and the other button near the bottom of the file) are missing an explicit type
and trigger lint errors; update both button elements to include type="button"
(instead of relying on default submit behavior) so they don’t submit forms
unintentionally and lint warnings are resolved.
- Around line 163-179: Replace the two raw <input> elements with the shared
Input component from frontend/src/components/ui/input.tsx to avoid duplicated
styling; import Input and use it for the email and password fields (e.g., <Input
id="email" placeholder="이메일을 입력하세요" type="email" autoComplete="off" /> and
<Input id="password" placeholder="••••••••" type="password" autoComplete="off"
/>), passing the same id/placeholder/type/autoComplete props and any needed
className or other props supported by Input (ensure Input forwards refs/props if
required) and remove the duplicated long className string from these files so
styling is maintained centrally.
- Line 21: The anchor elements using href="#" (e.g., the <a
className="text-white/90 hover:text-white text-sm font-medium
transition-colors"> element with text "문의하기" and the two other similar anchors)
cause accessibility/lint errors; replace each placeholder anchor either with a
proper route/link (e.g., Next.js <Link> or a real href to the intended page) or,
if the target is not implemented yet, convert the element to a <button
type="button"> with the same className and click handler to avoid invalid
navigation; update the three occurrences accordingly (the three <a href="#">
anchors in the login page JSX).
- Around line 141-146: The inline SVG has no accessible title for screen
readers; update the <svg> element (the Google logo SVG in the login page) to
include a <title> with a unique id (e.g., "google-logo-title") and add
aria-labelledby="google-logo-title" (and role="img" if not present) on the
<svg>, ensuring the title string describes the image (e.g., "Sign in with Google
logo").
In `@frontend/src/app/my-links/page.tsx`:
- Around line 68-86: The clickable folder tile currently uses a plain <div> with
onClick (key symbols: setSelectedFolderId and folder.memberFolderId) which lacks
keyboard and screen-reader accessibility; replace the div with a semantic
interactive element (preferably a <button> or a focusable role="button" element)
or add keyboard handlers and ARIA attributes: ensure the element is focusable
(tabindex if not using button), handle Enter/Space keydown to call
setSelectedFolderId(folder.memberFolderId), and add an accessible
name/aria-label and appropriate role to announce it to assistive tech while
preserving the existing className and visual styling (keep PINNED_COLORS usage
and inner structure).
- Around line 36-57: Several <button> elements in
frontend/src/app/my-links/page.tsx (e.g., the buttons containing the span icons
"search", "tune", "inventory_2", "mark_email_unread", "priority_high", "today"
and the other tag-styled buttons) are missing an explicit type attribute and
default to "submit"; add type="button" to each of these <button> elements so
they don't unintentionally submit forms. Locate the button elements by their
className patterns or the unique span material-symbols-outlined contents and add
type="button" to each instance listed in the review.
- Around line 166-186: The folder card div rendered in the folders?.map loop is
clickable via onClick but lacks keyboard accessibility; update the div (keyed by
folder.memberFolderId) to include role="button", tabIndex={0}, and an onKeyDown
handler that invokes setSelectedFolderId(folder.memberFolderId) when the user
presses Enter or Space (check e.key === 'Enter' or e.key === ' ' / keyCode
13/32) so keyboard users can activate the folder; ensure existing onClick
handler remains and don't change the more_horiz <button> element.
In `@frontend/src/app/page.tsx`:
- Around line 61-66: The inline SVG used in frontend/src/app/page.tsx (the
decorative Google-style icon SVG in the JSX) needs accessibility attributes:
mark it as purely decorative by adding aria-hidden="true" to the <svg> element
(or alternatively add a <title> and role="img" with an appropriate label if it
conveys information). Locate the SVG block (the <svg width="14" height="14"
viewBox="0 0 24 24"> ...</svg>) and update the element to include
aria-hidden="true" (or replace with <title> + role="img" if the icon is
informative).
- Line 26: Several <button> elements in page.tsx (e.g., the one with className
"md:hidden text-white" and the other buttons at the referenced locations) are
missing a type attribute which can cause unintended form submits; update each
bare <button> (search for buttons without a type in frontend/src/app/page.tsx,
including the "md:hidden text-white" button and the buttons near the other
referenced locations) to include type="button" so they do not act as implicit
submit buttons.
- Line 19: Replace the dummy href="#" on the "문의하기" anchor(s) with the real
target or proper behavior: locate the anchor element(s) with className
"text-white/90 hover:text-white text-sm font-medium transition-colors" and link
text "문의하기" (also the same occurrences around lines 292-294) and update them to
point to the correct route or external URL, or if they trigger a JS action
(modal/form) convert them to a <button> with appropriate role/aria attributes
and event handler; ensure accessibility by providing an explicit href or proper
ARIA attributes and keyboard interaction.
- Line 1: 현재 파일 상단의 "use client" 디렉티브를 삭제하세요; 이 페이지는 상태, 이펙트, 이벤트 핸들러가 없고 네비게이션이
NextLink 및 <a> 태그로만 이루어져 있으므로 클라이언트 런타임이 불필요합니다. "use client"를 제거한 뒤
useState/useEffect 등 클라이언트 전용 훅이나 브라우저 전용 API가 임포트되어 있지 않은지 확인하고, 버튼들에 onClick
핸들러가 전혀 없는지(모든 버튼이 정적 링크용인지) 검증하여 서버 컴포넌트로 유지하세요. 마지막으로 Tailwind 기반 호버/애니메이션만
사용하는지 확인하고, 필요없다면 어떤 추가 변경 없이 디렉티브만 제거하면 됩니다.
In `@frontend/src/components/dialogs/CreateFolderDialog.tsx`:
- Around line 88-96: The label in CreateFolderDialog.tsx isn't associated with
the input, so update the <label> and <input> pair to be accessible: either give
the input a unique id (e.g., id="folder-name") and add htmlFor="folder-name" to
the existing <label>, or wrap the <input> inside the <label> element; ensure the
value is still bound to folderName and onChange uses setFolderName.
- Around line 75-78: Buttons in the CreateFolderDialog JSX are missing an
explicit type which can cause unintended form submissions; locate the button
elements (e.g., the one with onClick={() => toggleCreateFolderDialog(false)} and
the other Cancel/close buttons inside the CreateFolderDialog component) and add
type="button" to each button element to prevent them from acting as submit
buttons.
- Around line 28-30: The component defines selectedIcon, selectedColor, and
isPinned but handleCreateFolder and the CreateFolderRequest type do not include
them; update the CreateFolderRequest interface to include icon: string, colorId:
string, and pinned: boolean (matching selectedIcon, selectedColor, isPinned) and
modify handleCreateFolder to include these fields in the payload sent to the API
(e.g., payload.icon = selectedIcon, payload.colorId = selectedColor,
payload.pinned = isPinned); if the backend doesn’t yet accept these fields,
either remove the unused state variables (selectedIcon, selectedColor, isPinned)
or coordinate with the backend to add support and handle validation/defaults
accordingly.
In `@frontend/src/components/dialogs/SaveLinkDialog.tsx`:
- Line 199: The label elements inside the SaveLinkDialog component (e.g., the
"URL" label and the other standalone labels) are not associated with input
controls and should be fixed for accessibility: either replace these purely
decorative/section-heading label tags with non-form elements like <p> or <span>,
or connect them to their corresponding inputs by adding htmlFor attributes that
match the input id attributes (ensure each input has a unique id). Locate these
labels within SaveLinkDialog.tsx and apply one of the two fixes consistently for
the labels at the same spots to eliminate misleading, unpaired <label> elements.
- Around line 281-285: The interactive folder tiles in SaveLinkDialog (the divs
keyed by folder.memberFolderId and using styles.folderTile with onClick that
calls setSelectedFolderId) are not keyboard-accessible; replace those clickable
divs with semantic <button> elements (or add role="button", tabIndex={0}, and
handle onKeyDown to trigger the same setSelectedFolderId logic) and ensure
aria-pressed/aria-selected reflects isActive; update all occurrences (the tiles
at the blocks around the setSelectedFolderId usage: the one using
folder.memberFolderId and the other similar tiles at the commented ranges) so
they are focusable, keyboard-operable, and maintain the same styling (apply
styles.folderTile and styles.folderTileActive to the button).
- Around line 64-85: The effect can apply an out-of-date analyzeUrlMutation
response to displayTitle when a previous request resolves after a newer URL; to
fix, track the latest request (e.g., store current url or an incrementing
requestId in a ref) before calling analyzeUrlMutation.mutate and, in the
onSuccess callback of analyzeUrlMutation.mutate, verify the response belongs to
the current request (compare the saved url or requestId) before calling
setDisplayTitle; touch the useEffect, analyzeUrlMutation.mutate call, and the
onSuccess handler (and use isTitleEdited/displayTitle checks as already present)
so only the latest URL’s title updates displayTitle.
In `@frontend/src/components/layout/Header.tsx`:
- Around line 16-24: The two plain <button> elements in Header (the
notifications button and the dark mode toggle which uses onClick={() =>
document.documentElement.classList.toggle('dark')}) are missing an explicit
type, causing them to default to type="submit" inside forms; update both button
elements to include type="button" to prevent accidental form submission while
preserving their existing className and onClick behavior.
In `@frontend/src/components/layout/Sidebar.tsx`:
- Around line 73-89: Add explicit type="button" attributes to the buttons to
avoid accidental form submissions: update the button that calls
useUIStore.getState().toggleCreateFolderDialog(true) and the button rendered
inside pinnedFolders.map that calls setSelectedFolderId(folder.memberFolderId)
so both have type="button"; this affects the elements associated with the
symbols useUIStore.getState().toggleCreateFolderDialog, pinnedFolders.map,
setSelectedFolderId and preserves existing className and handlers.
In `@frontend/src/components/my-links/FolderCard.tsx`:
- Around line 20-22: The dynamic Tailwind class group-hover:${color} in
FolderCard.tsx won’t be picked up by Tailwind JIT; replace it by mapping the
color prop to explicit Tailwind classes (e.g., a hoverClass map keyed by the
color values and returning strings like "group-hover:bg-blue-500"), or compute
the hover background via inline style if the color is arbitrary, or add the
possible patterns to tailwind.config.js safelist; update the JSX that builds the
container div (the element rendering {icon || <Folder size={20}/>}) to use the
resolved hover class name (or inline style) instead of the runtime-interpolated
group-hover:${color}.
In `@frontend/src/components/my-links/RightPanel.tsx`:
- Around line 204-214: The clickable card currently implemented as a <div> with
onClick in the RightPanel component is not keyboard accessible; update the
element(s) referenced by itemRef in the RightPanel (the card/folder/tag click
handlers around the current onClick blocks) to be a native <button> where
possible, or if you must keep a non-semantic element keep role="button",
tabindex={0} and add keyDown handler that triggers the same logic for Enter and
Space (mirroring the onClick behavior including bulk-select via onToggleSelect
and opening link.originalUrl), and ensure you preserve the className styling and
ARIA attributes (e.g., aria-pressed or aria-label) so keyboard and screen-reader
users get equivalent behavior.
- Around line 876-879: The bulk-delete handler fires
deleteBookmarkMutation.mutate for each id then immediately calls exitBulkMode(),
which hides failures; change to await the async deletions before exiting bulk
mode by using the mutation's async variant (e.g.,
deleteBookmarkMutation.mutateAsync) or return Promises and use
Promise.allSettled on selectedLinkIds.map(id =>
deleteBookmarkMutation.mutateAsync(id)), handle any rejections (show an error
toast or aggregate failures) and only call exitBulkMode() after
successful/settled results so partial failures are surfaced to the user;
reference selectedLinkIds, deleteBookmarkMutation.mutate/mutateAsync, and
exitBulkMode when making the change.
- Around line 150-153: The key handler treates both Enter and Escape as commit;
change handleNoteKeyDown so Enter calls handleNoteEditComplete() but Escape
performs a cancel-and-exit flow: restore the original note value (e.g., reset
the edit input state to the saved/original note copy) and close edit mode
instead of committing; call an existing cancel helper (create one if missing,
e.g., handleNoteEditCancel()) from handleNoteKeyDown for e.key === 'Escape' and
ensure it reverses any temp state and exits editing rather than invoking
handleNoteEditComplete().
- Around line 937-940: The onClick currently only calls
setIsMoveModalOpen(false) and exitBulkMode() but never invokes the move API or
updates the selected bookmarks' memberFolderId; modify the handler to call the
existing move function (e.g., moveBookmarks or moveSelectedItems) passing the
selected bookmark IDs (e.g., selectedBookmarkIds or selectedItems) and the
target folder id, update local state/Redux to set memberFolderId on those
bookmarks (or perform optimistic update), handle API success/failure (rollback
or show error), and only then close the modal with setIsMoveModalOpen(false) and
call exitBulkMode() after success (or still exit on cancel/error as
appropriate). Ensure you reference setIsMoveModalOpen and exitBulkMode in the
updated handler and include error handling/logging for the API call.
- Around line 311-318: The button in the RightPanel component (the inline
element that uses isFolderDropdownOpen and setIsFolderDropdownOpen) and other
button elements in this file lack an explicit type, which defaults to "submit"
in forms; update each <button> (including the one toggling the folder dropdown
that calls setIsFolderDropdownOpen and stops propagation) to include
type="button" to prevent accidental form submission if the component is later
rendered inside a form.
In `@frontend/src/components/ui/popover.tsx`:
- Around line 58-64: PopoverTitle currently types its props as
React.ComponentProps<"h2"> but renders a <div>, breaking semantic structure;
update the PopoverTitle component (function PopoverTitle) to render an <h2>
element instead of a <div> so the rendered element matches the declared props
and accessibility semantics, preserving the data-slot="popover-title", className
handling (cn("font-medium", className)) and spreading {...props} exactly as
before.
In `@frontend/src/lib/api/fetchClient.ts`:
- Around line 28-34: The current fetchClient code calls response.json()
unconditionally which will throw on 204 No Content; update the logic in the
function that parses the response (the block that assigns const json:
ApiResponse<T> = await response.json()) to first check for a no-content response
(e.g., if (response.status === 204 || response.headers.get('content-length') ===
'0') return undefined as unknown as T) and only call response.json() when there
is a body, so functions like deleteFolder that expect void/undefined won't error
on 204 responses.
In `@frontend/src/lib/api/folderApi.ts`:
- Around line 29-33: deleteFolder currently returns fetchClient<void>(...) which
can trigger JSON parse errors on 204/empty responses; change deleteFolder to
await the DELETE call and return void explicitly (e.g. await
fetchClient(`/api/folders/${folderId}`, { method: 'DELETE' }); return;), and
ensure fetchClient.ts is updated to treat empty/204 responses as no-content (do
not attempt JSON.parse on empty body). Reference functions: deleteFolder and
fetchClient (in fetchClient.ts).
In `@frontend/src/lib/auth/currentUser.ts`:
- Line 3: TEMP_MEMBER_ID is a hardcoded fallback member id that can cause all
unauthenticated requests to act as the same user; replace this by reading a
dev-only fallback from an env/config (e.g. DEV_FALLBACK_MEMBER_ID) and ensure
the code throws or returns no identity in production when no real auth is
present (check process.env.NODE_ENV === 'production' or an equivalent runtime
flag), and update any usages of TEMP_MEMBER_ID to use the new getter (e.g.
getFallbackMemberId()) so the fallback is only applied when NODE_ENV !==
'production' and the fallback env var is explicitly set.
In `@frontend/src/lib/store/linkStore.ts`:
- Around line 19-48: Extract the default filter object into a single constant
and reference it from the store to avoid duplication: create a const (e.g.,
DEFAULT_FILTERS) containing { searchQuery: '', selectedTags: [], sortType:
'latest' } and use it when initializing useLinkStore's filters and inside
clearFilters; ensure toggleTagFilter, setSearchQuery and setSortType still
spread state.filters (e.g., ...state.filters) so they merge correctly with
DEFAULT_FILTERS rather than hardcoding values.
In `@frontend/src/lib/types/apiResponse.ts`:
- Around line 2-9: Current ApiResponse<T> declares data: T always but backend
sends data: null on failures; convert ApiResponse to a discriminated union so
success=true carries data: T and error:null, and success=false carries data:null
and a populated error object (e.g., union of { success: true; data: T; error:
null } | { success: false; data: null; error: { code: string; message: string }
}). Update the ApiResponse type alias (replacing the existing interface) and
adjust any call sites that rely on ApiResponse<T> (e.g., consumers in
fetchClient.ts) to narrow on the success property before accessing data or
error.
In `@frontend/src/lib/types/bookmark.ts`:
- Line 23: The Bookmark types are inconsistent: BookmarkResponse currently uses
tags: string[] while the request interfaces
(CreateBookmarkRequest/UpdateBookmarkRequest) use tags?: string; make the
contract consistent by changing CreateBookmarkRequest and UpdateBookmarkRequest
to use tags?: string[] (or alternatively change BookmarkResponse to string if
you prefer backend shape) and add explicit serialization/deserialization where
network I/O occurs: when sending requests convert the tags string[] to the
backend-expected delimiter-separated string, and when receiving responses parse
the backend tags string into a string[] so BookmarkResponse consumers always get
string[].
In `@frontend/src/lib/types/folder.ts`:
- Around line 14-16: The CreateFolderRequest type currently exposes
ownerMemberId allowing clients to set ownership; remove ownerMemberId from the
client-facing type and ensure the server derives the owner ID from the
authenticated principal instead. Change the CreateFolderRequest interface to
only include parentFolderId (and other non-auth fields), update any places
constructing or accepting CreateFolderRequest (types/usages) to stop passing
ownerMemberId, and modify server-side controller/handler that reads
CreateFolderRequest to assign ownerMemberId from the authenticated user context
rather than from the request body.
In `@frontend/src/lib/types/tag.ts`:
- Around line 9-11: The CreateTagRequest type currently exposes ownerMemberId
which enables authorization bypass; remove ownerMemberId from CreateTagRequest
and only send tagName from the frontend, then ensure the backend’s
MemberTagController.create uses the authenticated principal (e.g.,
create(`@AuthenticationPrincipal` memberId, req.getTagName()) calling
memberTagService.create(memberId, tagName)) instead of trusting
request.ownerMemberId, and also add ownership checks in
MemberTagController.update() and MemberTagController.delete() to validate the
authenticated member owns the tag before calling memberTagService.update/delete.
In
`@src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java`:
- Around line 45-54: The duplicated temporary authentication bypass in
BookmarkApiController (the block setting memberId = 1L when currentUser is null
or "anonymousUser") should be removed from each endpoint and replaced with a
single helper method (e.g., resolveMemberId(currentUser) or
ensureAuthenticated(currentUser)) or handled via AOP around controller methods;
implement resolveMemberId to return the real ID by calling
getMemberId(currentUser), return an Optional or throw an appropriate exception
(Forbidden) when no ID, and ensure the temporary memberId = 1L bypass is
eliminated and flagged so it is not present in production.
- Around line 199-206: The analyzeUrl endpoint in BookmarkApiController (method
analyzeUrl) calls bookmarkService.extractTitle(url) without authentication or
URL safety checks, exposing an SSRF risk; restrict access by requiring
authenticated users (e.g., add `@PreAuthorize`("isAuthenticated()") or use
`@AuthenticationPrincipal` on the controller method) and harden extractTitle in
the BookmarkService to validate URLs before making requests (implement
allowlist/denylist, disallow private/internal IP ranges and localhost/metadata
IPs, and reject non-http(s) schemes); update tests to cover rejected unsafe URLs
and authorized access only.
In `@src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java`:
- Around line 109-110: Rename the method parameter from url to canonicalUrl in
MybatisBookmarkDao.selectLinkByCanonicalUrl to make intent clear: change the
signature public Link selectLinkByCanonicalUrl(String url) to use String
canonicalUrl and pass canonicalUrl into
mapper.selectLinkByCanonicalUrl(canonicalUrl); also update any local usages and
callers/overloads that reference this parameter name (or adjust tests) so names
remain consistent with the method purpose.
In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`:
- Around line 279-299: The extractTitle method (and the other Jsoup.connect
usage around lines 335-338) currently accepts user-provided URLs directly and
can cause SSRF and NPE; before calling Jsoup.connect or extractYoutubeTitle,
validate the input for null/blank and parse it with URI/URL to ensure a safe
scheme (only http/https), then resolve the hostname to IP(s) and block any
loopback, link-local, site-local, multicast, unspecified, or known metadata
addresses (e.g., 169.254.169.254) and private ranges; if any check fails, reject
the URL with an error. Apply the same validation/check function to both
extractTitle and the other methods that call Jsoup.connect, and only then
proceed to fetch; log validation failures and avoid attempting the fetch to
prevent SSRF.
In `@src/main/resources/application.properties`:
- Line 13: Remove the global DEBUG setting for the application package by
changing the default application.properties so it does not set
logging.level.com.web.SearchWeb=DEBUG (leave it as INFO or unset), and move the
DEBUG line into a profile-specific file (e.g., application-dev.properties or
application-local.properties) so that logging.level.com.web.SearchWeb=DEBUG is
only active for dev/local profiles; update documentation or README to mention
activating the dev/local profile to enable DEBUG.
---
Outside diff comments:
In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`:
- Around line 187-208: getOrCreateLink has a race between
selectLinkByCanonicalUrl and insertLink that can cause duplicate-insert
exceptions; wrap the insert (bookmarkDao.insertLink(newLink)) in a try/catch for
the database uniqueness exception (e.g., DataIntegrityViolationException or the
specific duplicate-key exception used) and on catch re-query
bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl) and return that existing
Link; keep the original successful return path for the newly inserted Link and
ensure canonicalUrl/newLink/insertLink/selectLinkByCanonicalUrl are the
referenced symbols to locate the change.
- Around line 217-224: BookmarkServiceImpl currently normalizes the URL via
normalizeUrl and fetches Link with
bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl) but then calls
bookmarkDao.checkBookmarkExistsByUrl(memberId, url) which has no MyBatis mapping
and risks mismatch; fix by adding a proper SQL mapper in
src/main/resources/mapper/bookmark-mapper.xml and change the service to reuse
the found Link (e.g., call a new checkBookmarkExistsByLinkId(memberId, linkId)
mapped in bookmark-mapper.xml) or alternatively add a
checkBookmarkExistsByCanonicalUrl(memberId, canonicalUrl) SQL mapping and call
that from BookmarkServiceImpl so the lookup is consistent with
selectLinkByCanonicalUrl.
ℹ️ Review info
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (7)
frontend/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlfrontend/public/file.svgis excluded by!**/*.svgfrontend/public/globe.svgis excluded by!**/*.svgfrontend/public/next.svgis excluded by!**/*.svgfrontend/public/vercel.svgis excluded by!**/*.svgfrontend/public/window.svgis excluded by!**/*.svgfrontend/src/app/favicon.icois excluded by!**/*.ico
📒 Files selected for processing (57)
build.gradledocs/Backend/01. backend-convention.mddocs/Fontend/01. frontend_architecture.mddocs/Plan/01. searchweb_PRD.mddocs/Plan/02. kpi_measurement_design.mdfrontend/.gitignorefrontend/README.mdfrontend/components.jsonfrontend/eslint.config.mjsfrontend/next.config.tsfrontend/package.jsonfrontend/pnpm-workspace.yamlfrontend/postcss.config.mjsfrontend/src/app/globals.cssfrontend/src/app/layout.tsxfrontend/src/app/login/page.tsxfrontend/src/app/my-links/page.tsxfrontend/src/app/page.tsxfrontend/src/components/dialogs/CreateFolderDialog.tsxfrontend/src/components/dialogs/SaveLinkDialog.tsxfrontend/src/components/layout/AppLayout.tsxfrontend/src/components/layout/Header.tsxfrontend/src/components/layout/Sidebar.tsxfrontend/src/components/my-links/FolderCard.tsxfrontend/src/components/my-links/RightPanel.tsxfrontend/src/components/providers/QueryProvider.tsxfrontend/src/components/ui/badge.tsxfrontend/src/components/ui/button.tsxfrontend/src/components/ui/dialog.tsxfrontend/src/components/ui/input.tsxfrontend/src/components/ui/popover.tsxfrontend/src/components/ui/select.tsxfrontend/src/components/ui/separator.tsxfrontend/src/components/ui/switch.tsxfrontend/src/components/ui/textarea.tsxfrontend/src/lib/api/bookmarkApi.tsfrontend/src/lib/api/fetchClient.tsfrontend/src/lib/api/folderApi.tsfrontend/src/lib/api/tagApi.tsfrontend/src/lib/auth/currentUser.tsfrontend/src/lib/store/folderStore.tsfrontend/src/lib/store/linkStore.tsfrontend/src/lib/store/uiStore.tsfrontend/src/lib/types/apiResponse.tsfrontend/src/lib/types/bookmark.tsfrontend/src/lib/types/folder.tsfrontend/src/lib/types/tag.tsfrontend/src/lib/utils.tsfrontend/tsconfig.jsonsrc/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.javasrc/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.javasrc/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.javasrc/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.javasrc/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.javasrc/main/resources/application.propertiessrc/main/resources/db/init_postgres.sqlsrc/main/resources/mapper/bookmark-mapper.xml
|
|
||
| 이 문서는 백엔드 전 도메인에 공통으로 적용하는 기본 컨벤션이다. | ||
|
|
||
| ## 1) 네이밍 규칙 |
There was a problem hiding this comment.
헤더 앞뒤 빈 줄을 추가해 markdownlint 경고(MD022)를 해소해 주세요.
Line 5, 10, 14, 20, 26, 31, 35, 39에서 헤더 주변 공백 규칙이 깨져 문서 lint 경고가 발생합니다.
Also applies to: 10-10, 14-14, 20-20, 26-26, 31-31, 35-35, 39-39
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 5-5: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/Backend/01`. backend-convention.md at line 5, The markdown headers
(e.g., "## 1) 네이밍 규칙") violate markdownlint MD022 because they lack a blank line
before and/or after the header; add a single empty line above and a single empty
line below each header occurrence (including the headers at the specified
locations such as the "## 1) 네이밍 규칙" header and the other headers referenced) so
every header has a blank line separating it from surrounding content to resolve
the lint warnings.
|
|
||
| ## ✅ 지금까지 완료된 작업 요약 | ||
|
|
||
| ### 1. 프로젝트 기반 및 환경 설계 |
There was a problem hiding this comment.
헤딩 전후 빈 줄 규칙(MD022)을 맞춰 주세요.
현재 문서는 CI markdownlint 경고가 발생하므로, 각 헤딩 아래에 빈 줄을 추가해 규칙을 통일하는 것이 좋습니다.
Also applies to: 47-47, 52-52, 87-87, 92-92, 97-97, 102-102, 108-108
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 42-42: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/Fontend/01`. frontend_architecture.md at line 42, Headings in the
document (e.g., "### 1. 프로젝트 기반 및 환경 설계" and the other headings referenced at
lines 47, 52, 87, 92, 97, 102, 108) violate MD022; ensure each heading is
surrounded by a single blank line above and below it (i.e., insert an empty line
before the heading if missing and an empty line after the heading), apply this
consistently for the listed headings so markdownlint no longer emits the MD022
warning.
| * 저장된 링크 중 AI 태그 포함 비율 **60% 이상** | ||
| * AI 추천 태그 채택률 **50% 이상** |
There was a problem hiding this comment.
헤더/펜스 전후 공백을 정리해 markdownlint 경고(MD022, MD031)를 제거해 주세요.
Line 71-72, 79-80, 87-88, 95-96, 111-112, 120-121, 129-130, 138-139 구간에서 동일 패턴의 경고가 반복됩니다.
Also applies to: 79-80, 87-88, 95-96, 111-112, 120-121, 129-130, 138-139
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 71-71: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 72-72: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/Plan/01`. searchweb_PRD.md around lines 71 - 72, Remove extra blank
lines and stray spaces immediately before and after Markdown headers and fenced
code blocks to satisfy markdownlint rules MD022 and MD031; specifically edit the
sections containing the bullet lines "* 저장된 링크 중 AI 태그 포함 비율 **60% 이상**" and "*
AI 추천 태그 채택률 **50% 이상**" (and the repeated occurrences at the other noted
ranges) so there is exactly one blank line separating headers/fences from
surrounding content and no trailing or leading spaces adjacent to header/fence
markers.
| > **문서 정보** | ||
| > - **버전:** v1.0.0 | ||
| > - **작성일:** 2026-02-22 | ||
| > - **근거 문서:** [searchweb_PRD.md](file:///e:/01.%20spring_projcet/SearchWeb/docs/Plan/searchweb_PRD.md) |
There was a problem hiding this comment.
로컬 절대경로 링크를 상대경로로 바꿔주세요.
Line 6의 file:///e:/... 링크는 저장소/CI/리뷰 환경에서 깨집니다.
수정 예시
-- **근거 문서:** [searchweb_PRD.md](file:///e:/01.%20spring_projcet/SearchWeb/docs/Plan/searchweb_PRD.md)
+- **근거 문서:** [01. searchweb_PRD.md](./01.%20searchweb_PRD.md)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| > - **근거 문서:** [searchweb_PRD.md](file:///e:/01.%20spring_projcet/SearchWeb/docs/Plan/searchweb_PRD.md) | |
| - **근거 문서:** [01. searchweb_PRD.md](./01.%20searchweb_PRD.md) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/Plan/02`. kpi_measurement_design.md at line 6, Replace the absolute
file:// link on the line containing the markdown link text "searchweb_PRD.md"
with a repository-relative path so the link works in CI/review environments;
change `file:///e:/.../searchweb_PRD.md` to a relative link like
`../Plan/searchweb_PRD.md` (or the correct relative path from docs/Plan/02.
kpi_measurement_design.md to the searchweb_PRD.md file) and ensure the markdown
link syntax remains valid.
| | :--- | :--- | :--- | :--- | | ||
| | `USER_SIGNUP` | 회원 가입 완료 | Backend | P0 | | ||
| | `USER_LOGIN` | 로그인 성공 | Backend | P0 | | ||
| | `PAGE_VIEW` | 페이지 진입 | Frontend | P0 | |
There was a problem hiding this comment.
익명 이벤트 정의와 테이블 제약이 충돌합니다.
Line 53에서 Frontend PAGE_VIEW를 수집 대상으로 정의했는데, Line 313-314의 member_id NOT NULL 제약이면 비로그인 이벤트 저장이 불가능합니다.
Also applies to: 313-314
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/Plan/02`. kpi_measurement_design.md at line 53, PAGE_VIEW is defined as
an anonymous Frontend event but the events table has a member_id NOT NULL
constraint, which prevents storing anonymous events; either make member_id
nullable in the schema (remove or change the NOT NULL constraint on member_id in
the events table) so PAGE_VIEW can be saved without a user, or change the
PAGE_VIEW definition to require authenticated users and ensure Frontend supplies
member_id for that event; update the schema constraint and/or the PAGE_VIEW
event definition (and any ingestion validation) so both definitions are
consistent.
| export interface CreateTagRequest { | ||
| ownerMemberId: number; | ||
| tagName: string; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find all files related to tags and CreateTagRequest
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.java" -o -name "*.kt" \) | xargs grep -l "CreateTagRequest\|ownerMemberId" | head -20Repository: Searchweb-Dev/Searchweb-Back
Length of output: 1257
🏁 Script executed:
# Search for CreateTagRequest usage
rg -n "CreateTagRequest" -A 5 -B 5Repository: Searchweb-Dev/Searchweb-Back
Length of output: 1641
🏁 Script executed:
# Search for backend tag creation endpoints and ownerMemberId handling
rg -n "ownerMemberId|create.*tag|tag.*create" -i --type java --type kt -C 3Repository: Searchweb-Dev/Searchweb-Back
Length of output: 98
🏁 Script executed:
# Check the tag.ts file to see the complete interface
cat frontend/src/lib/types/tag.ts 2>/dev/null || find . -name "tag.ts" -type f -exec cat {} \;Repository: Searchweb-Dev/Searchweb-Back
Length of output: 289
🏁 Script executed:
# Check the backend tag controller implementation
cat ./src/main/java/com/web/SearchWeb/tag/controller/MemberTagController.javaRepository: Searchweb-Dev/Searchweb-Back
Length of output: 2760
🏁 Script executed:
# Check the tag service implementation
cat ./src/main/java/com/web/SearchWeb/tag/service/MemberTagServiceImpl.javaRepository: Searchweb-Dev/Searchweb-Back
Length of output: 2183
🏁 Script executed:
# Check the MemberTagDto and domain class
cat ./src/main/java/com/web/SearchWeb/tag/controller/dto/MemberTagDto.javaRepository: Searchweb-Dev/Searchweb-Back
Length of output: 1258
🏁 Script executed:
# Check for authentication context usage in tag controller
rg -n "@.*Auth|SecurityContext|Principal|getCurrentUser|getAuthenticatedUser" ./src/main/java/com/web/SearchWeb/tag/ -A 3Repository: Searchweb-Dev/Searchweb-Back
Length of output: 54
CreateTagRequest의 ownerMemberId는 권한 위조(authorization bypass) 취약점입니다.
백엔드가 요청 바디의 ownerMemberId를 검증 없이 신뢰하므로, 사용자가 임의의 다른 사용자를 위해 태그를 생성할 수 있습니다. 태그 소유자는 반드시 서버의 인증된 사용자 컨텍스트에서 강제되어야 합니다.
🔐 제안 수정안
export interface CreateTagRequest {
- ownerMemberId: number;
tagName: string;
}백엔드에서 MemberTagController.create() 메서드는 현재 인증된 사용자를 주입받아 memberTagService.create(@AuthenticationPrincipal memberId, req.getTagName())로 호출해야 합니다. 또한 update(), delete() 메서드에서도 태그 소유권을 검증해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/lib/types/tag.ts` around lines 9 - 11, The CreateTagRequest type
currently exposes ownerMemberId which enables authorization bypass; remove
ownerMemberId from CreateTagRequest and only send tagName from the frontend,
then ensure the backend’s MemberTagController.create uses the authenticated
principal (e.g., create(`@AuthenticationPrincipal` memberId, req.getTagName())
calling memberTagService.create(memberId, tagName)) instead of trusting
request.ownerMemberId, and also add ownership checks in
MemberTagController.update() and MemberTagController.delete() to validate the
authenticated member owns the tag before calling memberTagService.update/delete.
| // 로그인 되지 않은 경우 (임시 바이패스: 1L 사용) | ||
| Long memberId; | ||
| if (currentUser == null || "anonymousUser".equals(currentUser)) { | ||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); | ||
| } | ||
|
|
||
| Long memberId = getMemberId(currentUser); | ||
| if (memberId == null) { | ||
| return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); | ||
| memberId = 1L; | ||
| } else { | ||
| memberId = getMemberId(currentUser); | ||
| if (memberId == null) { | ||
| return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); | ||
| } | ||
| } |
There was a problem hiding this comment.
인증 우회 로직이 6개 엔드포인트에서 중복됩니다.
동일한 임시 바이패스 로직(memberId = 1L)이 여러 메서드에 반복되어 있습니다. TODO 주석에서 언급된 대로 AOP 또는 별도의 헬퍼 메서드로 추출하는 것이 좋습니다.
또한, 이 임시 바이패스는 프로덕션에 배포되기 전에 반드시 제거해야 합니다. 모든 사용자가 memberId = 1의 데이터에 접근할 수 있는 심각한 보안 취약점입니다.
♻️ 헬퍼 메서드 추출 제안
+ /**
+ * 현재 사용자의 memberId를 추출하거나 임시 ID 반환 (개발 전용)
+ * TODO: 프로덕션 전 제거 필수
+ */
+ private Long resolveMemberId(Object currentUser) {
+ if (currentUser == null || "anonymousUser".equals(currentUser)) {
+ // WARNING: 개발 전용 - 프로덕션에서 제거 필수
+ return 1L;
+ }
+ return getMemberId(currentUser);
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java`
around lines 45 - 54, The duplicated temporary authentication bypass in
BookmarkApiController (the block setting memberId = 1L when currentUser is null
or "anonymousUser") should be removed from each endpoint and replaced with a
single helper method (e.g., resolveMemberId(currentUser) or
ensureAuthenticated(currentUser)) or handled via AOP around controller methods;
implement resolveMemberId to return the real ID by calling
getMemberId(currentUser), return an Optional or throw an appropriate exception
(Forbidden) when no ID, and ensure the temporary memberId = 1L bypass is
eliminated and flagged so it is not present in production.
| /** | ||
| * URL 분석 (제목 추출) | ||
| */ | ||
| @GetMapping("/analyze") | ||
| public ResponseEntity<ApiResponse<String>> analyzeUrl(@RequestParam String url) { | ||
| String title = bookmarkService.extractTitle(url); | ||
| return ResponseEntity.ok(ApiResponse.success(title)); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find extractTitle method implementation in BookmarkService
find . -name "*.java" -type f | xargs grep -l "extractTitle" | head -5Repository: Searchweb-Dev/Searchweb-Back
Length of output: 300
🏁 Script executed:
#!/bin/bash
# Search for extractTitle method definition with context
rg -n "def\s+extractTitle|public.*extractTitle" --type=java -A 30 -B 2Repository: Searchweb-Dev/Searchweb-Back
Length of output: 4125
🏁 Script executed:
#!/bin/bash
# Search for URL validation patterns in the codebase
rg -n "URI|URL|validate|whitelist|blacklist|allowlist" --type=java src/main/java/com/web/SearchWeb/bookmark/ | head -20Repository: Searchweb-Dev/Searchweb-Back
Length of output: 2350
🏁 Script executed:
#!/bin/bash
# Check if analyzeUrl endpoint has any authentication annotations
rg -n "@PreAuthorize|@Secured|getCurrentUserId|@RequestAttribute.*user" --type=java -B 5 -A 5 src/main/java/com/web/SearchWeb/bookmark/controller/Repository: Searchweb-Dev/Searchweb-Back
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Get full extractTitle method implementation
rg -n "public String extractTitle" --type=java -A 60 src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.javaRepository: Searchweb-Dev/Searchweb-Back
Length of output: 2866
🏁 Script executed:
#!/bin/bash
# Check BookmarkApiController for class-level `@PreAuthorize` or other security annotations
rg -n "class BookmarkApiController|@PreAuthorize|@RequestAttribute|getCurrentUserId" --type=java -B 10 -A 20 src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java | head -60Repository: Searchweb-Dev/Searchweb-Back
Length of output: 940
🏁 Script executed:
#!/bin/bash
# Search for any SSRF filtering, IP blocking, or domain whitelisting in bookmark service
rg -n "localhost|127.0.0.1|169.254|internal|whitelist|blacklist|allowlist|filter.*url|block.*url" --type=java src/main/java/com/web/SearchWeb/bookmark/Repository: Searchweb-Dev/Searchweb-Back
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Final verification: check analyzeUrl endpoint in context of other endpoints
cat -n src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java | sed -n '195,210p'Repository: Searchweb-Dev/Searchweb-Back
Length of output: 630
🏁 Script executed:
#!/bin/bash
# Search for any global security filters or interceptors that might protect all endpoints
rg -n "SecurityConfig|WebSecurityConfigurerAdapter|addInterceptors|setInterceptors" --type=java src/main/java/com/web/SearchWeb/ | head -10Repository: Searchweb-Dev/Searchweb-Back
Length of output: 302
/api/bookmarks/analyze 엔드포인트에 인증이 없고 SSRF에 노출되어 있습니다.
이 엔드포인트는 인증 없이 extractTitle() 메서드를 통해 임의의 외부 URL에 대한 HTTP 요청을 트리거합니다. 악의적인 사용자가 내부 네트워크 리소스(예: http://localhost:8080, http://169.254.169.254/ AWS 메타데이터)에 접근하거나 내부 서비스를 스캔하는 데 악용할 수 있습니다.
해결 방안:
- 인증된 사용자만 이 기능을 사용하도록
@PreAuthorize또는@AuthenticationPrincipal추가 extractTitle()메서드에 URL 검증 로직 추가 (허용 목록/차단 목록, 내부 IP 필터링)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java`
around lines 199 - 206, The analyzeUrl endpoint in BookmarkApiController (method
analyzeUrl) calls bookmarkService.extractTitle(url) without authentication or
URL safety checks, exposing an SSRF risk; restrict access by requiring
authenticated users (e.g., add `@PreAuthorize`("isAuthenticated()") or use
`@AuthenticationPrincipal` on the controller method) and harden extractTitle in
the BookmarkService to validate URLs before making requests (implement
allowlist/denylist, disallow private/internal IP ranges and localhost/metadata
IPs, and reject non-http(s) schemes); update tests to cover rejected unsafe URLs
and authorized access only.
| public Link selectLinkByCanonicalUrl(String url) { | ||
| return mapper.selectLinkByCanonicalUrl(url); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
파라미터명을 canonicalUrl로 맞추면 의도가 더 명확해집니다.
현재 시그니처는 동작상 문제 없지만, 용어 일치로 오해 가능성을 줄일 수 있습니다.
🧹 제안 수정안
- public Link selectLinkByCanonicalUrl(String url) {
- return mapper.selectLinkByCanonicalUrl(url);
+ public Link selectLinkByCanonicalUrl(String canonicalUrl) {
+ return mapper.selectLinkByCanonicalUrl(canonicalUrl);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public Link selectLinkByCanonicalUrl(String url) { | |
| return mapper.selectLinkByCanonicalUrl(url); | |
| public Link selectLinkByCanonicalUrl(String canonicalUrl) { | |
| return mapper.selectLinkByCanonicalUrl(canonicalUrl); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java` around
lines 109 - 110, Rename the method parameter from url to canonicalUrl in
MybatisBookmarkDao.selectLinkByCanonicalUrl to make intent clear: change the
signature public Link selectLinkByCanonicalUrl(String url) to use String
canonicalUrl and pass canonicalUrl into
mapper.selectLinkByCanonicalUrl(canonicalUrl); also update any local usages and
callers/overloads that reference this parameter name (or adjust tests) so names
remain consistent with the method purpose.
|
|
||
| #logging.level.org.springframework.security=TRACE | ||
| logging.level.org.springframework.security=DEBUG | ||
| logging.level.com.web.SearchWeb=DEBUG |
There was a problem hiding this comment.
기본 설정에서 DEBUG 로그 레벨 고정은 운영 리스크가 큽니다.
Line 13에서 애플리케이션 패키지 로그를 전역 DEBUG로 설정하면 운영 환경에서도 과도한 로그/민감정보 노출 가능성이 생깁니다. 기본값은 INFO로 두고, DEBUG는 dev/local 프로필에서만 활성화해 주세요.
권장 수정안
-logging.level.com.web.SearchWeb=DEBUG
+logging.level.com.web.SearchWeb=INFO# application-dev.properties 또는 application-local.properties
logging.level.com.web.SearchWeb=DEBUG📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| logging.level.com.web.SearchWeb=DEBUG | |
| logging.level.com.web.SearchWeb=INFO |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/resources/application.properties` at line 13, Remove the global
DEBUG setting for the application package by changing the default
application.properties so it does not set logging.level.com.web.SearchWeb=DEBUG
(leave it as INFO or unset), and move the DEBUG line into a profile-specific
file (e.g., application-dev.properties or application-local.properties) so that
logging.level.com.web.SearchWeb=DEBUG is only active for dev/local profiles;
update documentation or README to mention activating the dev/local profile to
enable DEBUG.
- [coderabbitai] apiResponse.ts: Discriminated Union 패턴을 적용하여 성공/실패 응답의 타입 불일치 및 잠재적 런타임 에러 방지 - fetchClient: 204 No Content 및 빈 응답 시 JSON 파싱 에러 방지 처리 추가 - 코드 리뷰 피드백 반영 및 가독성을 위한 한글 주석 적용
- [coderabbitai] 스크린 리더 등 보조기기 지원을 위해 label(Folder Name)과 input 요소를 htmlFor 및 id 속성으로 명시적 연결 - 코드 리뷰 피드백을 반영하여 시맨틱 마크업 권장 사항 준수
- globals.css와 공통 UI 컴포넌트(Popover)의 세부 디자인 토큰 수정 - 로그인 및 북마크 대시보드(마이링크) 페이지의 뷰 레이아웃 최적화 - README 설명 최신화
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aca9124ac5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| onClick={() => { | ||
| setIsMoveModalOpen(false); | ||
| exitBulkMode(); | ||
| }} |
There was a problem hiding this comment.
Perform folder update when confirming bulk move
Selecting a destination in the bulk-move modal currently just closes the modal and exits bulk edit mode, but never calls updateBookmarkMutation (or any other API) for selectedLinkIds. In practice, users can select links, click Move, pick a folder, and see no data change, which is a silent functional failure of a primary bulk action.
Useful? React with 👍 / 👎.
| analyzeUrlMutation.mutate(url, { | ||
| onSuccess: (realTitle: string) => { | ||
| if (!isTitleEdited && realTitle) { | ||
| setDisplayTitle(realTitle); | ||
| } |
There was a problem hiding this comment.
Guard title autofill against stale analyze responses
The URL analysis effect fires a new analyzeUrl mutation on every URL change, but the success callback always writes setDisplayTitle(realTitle) without checking that the response still matches the latest input. If a user edits/pastes URLs quickly (or network responses return out of order), an older response can overwrite the current URL title, leading to incorrect titles being shown and saved.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 25
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java (1)
217-224:⚠️ Potential issue | 🟠 Major존재 여부 체크가 canonical URL 전략과 불일치합니다.
Line 224에서 원본
url로 다시 조회하면 정규화된 동일 URL을 놓칠 수 있습니다. canonical 기준으로 일관되게 조회하세요.권장 수정안
- return bookmarkDao.checkBookmarkExistsByUrl(memberId, url) > 0; + return bookmarkDao.checkBookmarkExistsByUrl(memberId, canonicalUrl) > 0;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java` around lines 217 - 224, The existence check uses the original url instead of the normalized canonicalUrl, causing misses; update the logic in BookmarkServiceImpl so after obtaining canonicalUrl (via normalizeUrl) and confirming a Link exists (selectLinkByCanonicalUrl), you perform the bookmark existence check using the canonical identifier (e.g., pass canonicalUrl to bookmarkDao.checkBookmarkExistsByUrl or use the Link's id from selectLinkByCanonicalUrl) instead of the raw url (memberId and url), so the check is consistent with the canonicalization strategy.
♻️ Duplicate comments (17)
src/main/resources/application.properties (1)
13-13:⚠️ Potential issue | 🟠 Major기본 설정에서 애플리케이션 DEBUG 로그 고정은 운영 리스크입니다.
Line 13의 설정은 운영 환경에서도
com.web.SearchWeb로그를 DEBUG로 강제할 수 있어, 로그량 급증 및 민감정보 노출 위험이 있습니다. 기본값은 INFO(또는 미설정)로 두고 DEBUG는application-dev.properties/application-local.properties로 분리해 주세요.권장 수정안
-logging.level.com.web.SearchWeb=DEBUG +# logging.level.com.web.SearchWeb=INFO# application-dev.properties 또는 application-local.properties logging.level.com.web.SearchWeb=DEBUG🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/application.properties` at line 13, Replace the hardcoded DEBUG setting in application.properties for logging.level.com.web.SearchWeb with a non-DEBUG default (e.g., remove the key or set to INFO) and move the DEBUG override into profile-specific files (e.g., application-dev.properties or application-local.properties) so that DEBUG is only enabled in dev/local environments; update any documentation or README that references logging setup to reflect the profile-based DEBUG configuration.frontend/next.config.ts (1)
5-11:⚠️ Potential issue | 🟠 Major백엔드 URL이 하드코딩되어 배포 환경에서 문제가 발생합니다.
http://localhost:8080이 고정되어 있어 스테이징/프로덕션 환경에서 API 호출이 실패합니다. 환경 변수로 분리해야 합니다.수정 제안
import type { NextConfig } from "next"; +const backendApiOrigin = + process.env.BACKEND_API_ORIGIN ?? + (process.env.NODE_ENV === "development" ? "http://localhost:8080" : undefined); + const nextConfig: NextConfig = { // [BACKEND_CONNECT] 백엔드 API 프록시 설정 (CORS 해결) async rewrites() { + if (!backendApiOrigin) return []; + return [ { source: '/api/:path*', - destination: 'http://localhost:8080/api/:path*', + destination: `${backendApiOrigin}/api/:path*`, }, ]; }, // [/BACKEND_CONNECT] };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/next.config.ts` around lines 5 - 11, The rewrites() in next.config.ts currently hardcodes 'http://localhost:8080' causing failures in staging/production; change rewrites() to build the destination from an environment variable (e.g., process.env.BACKEND_URL or NEXT_PUBLIC_BACKEND_URL) with a sensible localhost default for local dev, and update any runtime config or .env docs so BACKEND_URL is set in each environment; specifically modify the destination for source '/api/:path*' to use that env var (keeping the '/api/:path*' suffix) so deployments point to the correct backend URL.frontend/src/lib/auth/currentUser.ts (1)
3-3:⚠️ Potential issue | 🟠 Major하드코딩된 회원 ID를 프로덕션에서 차단하세요.
현재 값은 인증 누락 시 모든 요청이 동일 사용자로 처리될 수 있습니다. 개발 환경에서만 명시적 fallback을 허용하고, 프로덕션에서는 인증 정보가 없으면 실패 처리해야 합니다.
🔧 제안 수정안
-export const TEMP_MEMBER_ID = 1; +const devFallbackRaw = process.env.NEXT_PUBLIC_DEV_MEMBER_ID; + +export function getCurrentMemberId(): number { + if (process.env.NODE_ENV !== "production" && devFallbackRaw) { + const parsed = Number(devFallbackRaw); + if (Number.isInteger(parsed) && parsed > 0) return parsed; + } + throw new Error("인증 정보가 없습니다. 실제 세션/토큰 연동이 필요합니다."); +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/lib/auth/currentUser.ts` at line 3, TEMP_MEMBER_ID is a hardcoded fallback that can cause all requests to be treated as the same user; update the exported TEMP_MEMBER_ID handling so that in production it does not provide a fallback (throw or return null/error when auth is missing) and only returns a safe explicit fallback when NODE_ENV === 'development' or a dedicated DEV_FALLBACK flag is set; modify currentUser.ts to replace the unconditional export with logic that reads process.env.NODE_ENV or a DEV flag, returns the temporary id only in dev, and rejects/throws or returns undefined in production so callers must handle missing authentication.frontend/src/app/page.tsx (4)
1-1: 🧹 Nitpick | 🔵 Trivial불필요한
"use client"선언을 제거하세요.현재 페이지는 상태/이펙트/이벤트 핸들러 없이 정적 렌더링 중심이어서 서버 컴포넌트로 충분합니다.
#!/bin/bash set -euo pipefail # 클라이언트 전용 훅/이벤트 핸들러 사용 여부 확인 rg -n 'use(State|Effect|Reducer|Ref|Memo|Callback)\(|on[A-Z][A-Za-z]*=' frontend/src/app/page.tsx🔧 제안 수정안
-"use client"; - import NextLink from "next/link";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/app/page.tsx` at line 1, 현재 파일에 불필요한 "use client" 디렉티브가 있어 이 페이지는 서버 컴포넌트로 충분하므로 최상단의 "use client" 문자열을 제거하세요; 제거 후 page.tsx에서 useState/useEffect/useRef/useMemo/useCallback 또는 onX 이벤트 핸들러 같은 클라이언트 전용 API가 사용되지 않는지 (예: 검색 정규식으로 검사한 결과) 확인하고, 만약 있다면 해당 로직을 클라이언트 컴포넌트로 분리하거나 제거하여 서버 컴포넌트 규약을 유지하도록 수정하세요.
61-66:⚠️ Potential issue | 🟡 Minor장식용 SVG 접근성 속성을 보강하세요.
해당 아이콘은 장식용이므로 스크린리더에서 제외되도록 처리해야 합니다.
🔧 제안 수정안
-<svg width="14" height="14" viewBox="0 0 24 24"> +<svg width="14" height="14" viewBox="0 0 24 24" aria-hidden="true" focusable="false">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/app/page.tsx` around lines 61 - 66, The SVG used as a decorative icon should be hidden from assistive tech; update the <svg> element in frontend/src/app/page.tsx (the inline Google-logo SVG) to include accessibility attributes such as aria-hidden="true" and focusable="false" (and remove any title/desc that would expose it) so screen readers ignore it; ensure you apply these attributes directly on the svg tag that contains the four <path> children.
26-26:⚠️ Potential issue | 🟡 Minor버튼
type을 명시하세요.폼 컨텍스트에서 의도치 않은 submit을 방지하려면
type="button"이 필요합니다.🔧 제안 수정안
-<button className="md:hidden text-white"> +<button type="button" className="md:hidden text-white"> -<button className="flex items-center justify-center gap-2 bg-white hover:bg-slate-50 text-text-sub border border-slate-200 text-sm font-semibold h-11 px-6 rounded-lg transition-all whitespace-nowrap"> +<button type="button" className="flex items-center justify-center gap-2 bg-white hover:bg-slate-50 text-text-sub border border-slate-200 text-sm font-semibold h-11 px-6 rounded-lg transition-all whitespace-nowrap"> -<button className="mb-8 w-full rounded-lg bg-slate-100 px-4 py-3 text-sm font-bold text-text-main transition-all hover:bg-[linear-gradient(135deg,`#6d28d9_0`%,`#8b5cf6_50`%,`#a78bfa_100`%)] hover:text-white hover:shadow-lg hover:shadow-primary/20"> +<button type="button" className="mb-8 w-full rounded-lg bg-slate-100 px-4 py-3 text-sm font-bold text-text-main transition-all hover:bg-[linear-gradient(135deg,`#6d28d9_0`%,`#8b5cf6_50`%,`#a78bfa_100`%)] hover:text-white hover:shadow-lg hover:shadow-primary/20"> -<button className="mb-8 w-full rounded-lg bg-[linear-gradient(135deg,`#6d28d9_0`%,`#8b5cf6_50`%,`#a78bfa_100`%)] px-4 py-3 text-sm font-bold text-white transition-all hover:brightness-110 shadow-lg shadow-primary/25 border-t border-white/20"> +<button type="button" className="mb-8 w-full rounded-lg bg-[linear-gradient(135deg,`#6d28d9_0`%,`#8b5cf6_50`%,`#a78bfa_100`%)] px-4 py-3 text-sm font-bold text-white transition-all hover:brightness-110 shadow-lg shadow-primary/25 border-t border-white/20"> -<button className="mb-8 w-full rounded-lg bg-slate-100 px-4 py-3 text-sm font-bold text-text-main transition-all hover:bg-[linear-gradient(135deg,`#6d28d9_0`%,`#8b5cf6_50`%,`#a78bfa_100`%)] hover:text-white hover:shadow-lg hover:shadow-primary/20"> +<button type="button" className="mb-8 w-full rounded-lg bg-slate-100 px-4 py-3 text-sm font-bold text-text-main transition-all hover:bg-[linear-gradient(135deg,`#6d28d9_0`%,`#8b5cf6_50`%,`#a78bfa_100`%)] hover:text-white hover:shadow-lg hover:shadow-primary/20">Also applies to: 70-70, 193-193, 224-224, 256-256
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/app/page.tsx` at line 26, Several plain <button> elements in the page component are missing an explicit type and can trigger unintended form submissions; update each button element (e.g., the one with className "md:hidden text-white" and the other similar buttons flagged in the review) to include type="button" so they do not act as submit buttons inside form contexts, ensuring you add the attribute to every <button> instance noted in the diff.
19-19:⚠️ Potential issue | 🟡 Minor
href="#"링크를 실제 목적지로 교체하세요.현재 링크는 내비게이션 목적지가 없어 접근성과 사용성이 모두 저하됩니다.
🔧 제안 수정안
-<a className="text-white/90 hover:text-white text-sm font-medium transition-colors" href="#">문의하기</a> +<a className="text-white/90 hover:text-white text-sm font-medium transition-colors" href="/contact">문의하기</a> -<a className="hover:text-primary transition-colors" href="#">이용약관</a> -<a className="hover:text-primary transition-colors" href="#">개인정보처리방침</a> -<a className="hover:text-primary transition-colors" href="#">문의하기</a> +<a className="hover:text-primary transition-colors" href="/terms">이용약관</a> +<a className="hover:text-primary transition-colors" href="/privacy">개인정보처리방침</a> +<a className="hover:text-primary transition-colors" href="/contact">문의하기</a>Also applies to: 292-294
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/app/page.tsx` at line 19, The anchor elements using href="#" (the <a className="text-white/90 hover:text-white text-sm font-medium transition-colors" href="#">문의하기</a> and the duplicate occurrences) must be replaced with real targets or appropriate controls: update each anchor's href to the actual route or external URL (e.g., "/contact" or "mailto:...") or, if it triggers a JS action rather than navigation, replace with a button element or an accessible element with role="button" and proper onClick handler; ensure to update the instances referenced by the symbol of that anchor in this file and the duplicate occurrences (lines noted in the review) so the links are navigable and accessible.docs/Fontend/01. frontend_architecture.md (1)
42-42:⚠️ Potential issue | 🟡 Minor헤딩 전후 공백 규칙(MD022)을 맞춰 주세요.
해당 헤딩들 아래에 빈 줄이 없어 markdownlint 경고가 발생합니다. 각 헤딩의 위/아래 공백 1줄을 일관되게 적용해 주세요.
🔧 예시 수정
### 1. 프로젝트 기반 및 환경 설계 + - **초기화**: Next.js 15 + React 19 + TypeScript Strict 모드 설정Also applies to: 47-47, 52-52, 87-87, 92-92, 97-97, 102-102, 108-108
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/Fontend/01`. frontend_architecture.md at line 42, Add a single blank line before and after each top-level/subheading to satisfy markdownlint MD022: for example ensure there is one empty line above and below "### 1. 프로젝트 기반 및 환경 설계" and do the same for the other affected headings (the headings at the other flagged locations), so every heading has exactly one blank line surrounding it; update the markdown accordingly and re-run lint to confirm the MD022 warnings are resolved.docs/Backend/01. backend-convention.md (1)
5-5:⚠️ Potential issue | 🟡 Minor헤딩 아래 빈 줄을 추가해 MD022 경고를 해소하세요.
각 헤딩 바로 다음 줄에 본문/리스트가 시작되어 markdownlint 경고가 발생합니다. 헤딩 전후 1줄 공백 규칙을 통일해 주세요.
🔧 예시 수정
## 1) 네이밍 규칙 + - Java 클래스명: `UpperCamelCase` (예: `BoardService`)Also applies to: 10-10, 14-14, 20-20, 26-26, 31-31, 35-35, 39-39
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/Backend/01`. backend-convention.md at line 5, Add a single blank line immediately after the markdown heading "## 1) 네이밍 규칙" (and likewise after any other headings in this file that are directly followed by content) to satisfy MD022; find occurrences where a heading is followed on the next line by text or a list and insert one empty line between the heading and the content so all headings have a blank line beneath them.docs/Plan/01. searchweb_PRD.md (1)
16-312:⚠️ Potential issue | 🟡 Minormarkdownlint 경고(MD022/MD058/MD047)를 한 번에 정리해 주세요.
현재 헤더/테이블 주변 공백 규칙 위반이 반복되고, 파일 마지막 개행도 누락된 상태라 문서 린트가 계속 실패할 수 있습니다. 헤더/테이블 전후 1줄 공백과 파일 끝 단일 개행을 일괄 정리해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/Plan/01`. searchweb_PRD.md around lines 16 - 312, The document violates markdownlint rules MD022/MD058/MD047 due to missing blank lines around headers and tables and a missing final newline; fix by ensuring there is exactly one empty line before and after each header (e.g., "### 🗺️ MVP 우선순위 및 핵심 가치 (Roadmap)", "## 2. Problem & Hypothesis", "## 3. Goal", etc.) and before/after every table block (the pipe-style tables under Problem & Hypothesis, FR sections, etc.), remove any extra multiple blank lines so only a single blank line surrounds those blocks, and add a single newline character at the end of the file to ensure a final newline.frontend/src/components/dialogs/CreateFolderDialog.tsx (2)
27-30:⚠️ Potential issue | 🟠 Major선택한 아이콘/색상/고정 상태가 생성 요청에 반영되지 않습니다.
사용자가 조정한 UI 상태가 저장되지 않아 기능 기대치와 실제 동작이 어긋납니다. API가 지원하면 payload에 포함하고, 미지원이면 해당 UI를 숨기거나 제거해야 합니다.
🛠️ 제안 수정안(백엔드 지원 시)
createFolderMutation.mutate( { ownerMemberId: TEMP_MEMBER_ID, // 현재는 테스트용 ID 사용 folderName: folderName.trim(), + icon: selectedIcon, + colorId: selectedColor, + pinned: isPinned, }, { // 성공 시 후처리 onSuccess: () => { toggleCreateFolderDialog(false); // 창 닫기 setFolderName(''); // 입력값 초기화 + setSelectedIcon('work'); + setSelectedColor('purple'); + setIsPinned(true); }, } );Also applies to: 44-48
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/dialogs/CreateFolderDialog.tsx` around lines 27 - 30, The UI state for selectedIcon, selectedColor, and isPinned is not being sent to the backend; update the folder creation submit handler (e.g., the function that dispatches createFolder / handleCreate / onCreateSubmit in CreateFolderDialog.tsx) to include folderName, selectedIcon, selectedColor and isPinned in the API payload, ensuring the same state variables (selectedIcon, selectedColor, isPinned, folderName) are referenced; if the backend API does not accept those fields, remove or hide the icon/color/pinned controls from the dialog UI (or gate them behind a feature flag) so the UI reflects available backend support.
75-78:⚠️ Potential issue | 🟡 Minor여러 버튼에
type="button"이 누락되었습니다.폼 내부 기본 submit 동작이 섞여 의도치 않은 제출을 유발할 수 있습니다.
🛠️ 제안 수정안
- <button + <button type="button" onClick={() => toggleCreateFolderDialog(false)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors" > - <button + <button type="button" key={iconStr} onClick={() => setSelectedIcon(iconStr)} style={{ width: '36px', height: '36px', minWidth: '36px', minHeight: '36px' }} className={`flex items-center justify-center rounded-lg transition-all shadow-sm flex-shrink-0 ${ ... }`} > - <button + <button type="button" key={c.id} onClick={() => setSelectedColor(c.id)} className={`w-6 h-6 rounded-full flex-shrink-0 transition-all dark:ring-offset-gray-800 ring-offset-2 ${c.base} ${ ... }`} ></button>Also applies to: 112-121, 157-163
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/dialogs/CreateFolderDialog.tsx` around lines 75 - 78, Multiple buttons inside CreateFolderDialog (e.g., the close button with onClick={() => toggleCreateFolderDialog(false)} and other non-submit buttons later in the component) are missing type="button", which can trigger unwanted form submission; update every non-submit button in the CreateFolderDialog component to include type="button" (for example add type="button" to the button that calls toggleCreateFolderDialog(false) and to the other action buttons in this component) so only the intended submit control triggers form submit.frontend/src/lib/types/tag.ts (1)
9-11:⚠️ Potential issue | 🔴 Critical
CreateTagRequest에서ownerMemberId를 제거해야 합니다.태그 소유자는 클라이언트 입력이 아니라 서버 인증 컨텍스트에서 강제되어야 합니다. 현재 형태는 권한 위조 경로를 열 수 있습니다.
🔐 제안 수정안
export interface CreateTagRequest { - ownerMemberId: number; tagName: string; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/lib/types/tag.ts` around lines 9 - 11, The CreateTagRequest type exposes ownerMemberId which must be removed so ownership is enforced server-side; delete the ownerMemberId field from the CreateTagRequest interface and update all call sites that construct or type-check CreateTagRequest (e.g., tag creation helpers, API client methods, and any components or tests) to stop passing or expecting ownerMemberId, instead rely on server/auth context to set the owner; ensure compile errors are fixed by updating signatures like the createTag API call and related serializers/parsers to reflect the new shape.frontend/src/app/login/page.tsx (1)
22-22:⚠️ Potential issue | 🟠 Major플레이스홀더 링크(
href="#")를 실제 경로 또는 버튼으로 바꿔야 합니다.현재 값은 유효한 내비게이션이 아니며 접근성/린트 이슈를 유발합니다. 미구현 상태면
button type="button"으로 전환하세요.🛠️ 제안 수정안(예시)
- <a className="text-white/90 hover:text-white text-sm font-medium transition-colors" href="#">문의하기</a> + <button type="button" className="text-white/90 hover:text-white text-sm font-medium transition-colors">문의하기</button> - <NextLink className="text-[11px] font-semibold text-gray-300 hover:text-primary-dark" href="#">Forgot password?</NextLink> + <button type="button" className="text-[11px] font-semibold text-gray-300 hover:text-primary-dark">Forgot password?</button> - Don't have an account? <NextLink className="font-bold text-primary transition-colors hover:text-primary-dark" href="#">Sign up</NextLink> + Don't have an account? <button type="button" className="font-bold text-primary transition-colors hover:text-primary-dark">Sign up</button>Also applies to: 183-183, 195-195
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/app/login/page.tsx` at line 22, The anchor using href="#" for the "문의하기" link in page.tsx is a placeholder causing accessibility/lint issues; replace it with a real navigation or a button: either change the element to a proper Link/anchor with the real path (e.g., "/contact") using your routing method, or convert the <a ... href="#"> element with className "text-white/90 hover:text-white text-sm font-medium transition-colors" and inner text "문의하기" into a <button type="button"> and wire the click to navigate (e.g., router.push or onClick handler). Apply the same change for the other duplicate occurrences (the other "문의하기" anchors noted in the file).frontend/src/components/layout/Sidebar.tsx (2)
82-89:⚠️ Potential issue | 🟡 MinorPinned 폴더 버튼에도
type="button"속성 추가 필요동일하게
type="button"속성이 누락되어 있습니다.🔧 수정 제안
{pinnedFolders.map((folder, idx) => ( <button + type="button" key={folder.memberFolderId} onClick={() => setSelectedFolderId(folder.memberFolderId)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/layout/Sidebar.tsx` around lines 82 - 89, The pinned folder button in Sidebar (the <button key={folder.memberFolderId} onClick={() => setSelectedFolderId(folder.memberFolderId)} ...> element) is missing an explicit type and should have type="button" added so clicking it does not trigger form submission; update that button element in Sidebar.tsx to include type="button" alongside its existing props.
73-78:⚠️ Potential issue | 🟡 Minor버튼 요소에
type="button"속성 추가 필요이 버튼에
type속성이 누락되어 있습니다. 폼 내부에서 의도치 않은 제출을 방지하기 위해 명시적으로 추가해 주세요.🔧 수정 제안
<button + type="button" onClick={() => useUIStore.getState().toggleCreateFolderDialog(true)} className="text-gray-500 hover:text-white transition-colors" >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/layout/Sidebar.tsx` around lines 73 - 78, The button in Sidebar.tsx that calls useUIStore.getState().toggleCreateFolderDialog(true) is missing an explicit type and can accidentally submit a surrounding form; update the <button> element rendering the "add" icon to include type="button" so it won't act as a submit button when placed inside a form.frontend/src/lib/types/folder.ts (1)
14-16:⚠️ Potential issue | 🔴 Critical
CreateFolderRequest에서ownerMemberId를 제거해야 합니다.클라이언트가 소유자 ID를 임의 지정할 수 있어 권한 상승으로 이어질 수 있습니다. 소유자는 인증 주체에서 서버가 결정해야 합니다.
권장 수정안
export interface CreateFolderRequest { - ownerMemberId: number; parentFolderId?: number | null; folderName: string; description?: string; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/lib/types/folder.ts` around lines 14 - 16, The CreateFolderRequest interface exposes ownerMemberId which allows clients to set the folder owner; remove ownerMemberId from the CreateFolderRequest type definition and any client-side construction of that payload (search for usages of CreateFolderRequest and ownerMemberId), update API call sites to stop sending ownerMemberId, and ensure server-side folder creation uses the authenticated user (not client-provided owner) to set the owner; also update any tests/types that referenced ownerMemberId accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@build.gradle`:
- Line 37: The added dependency implementation 'org.jsoup:jsoup:1.17.2' is
appropriate and safe for HTML title extraction; no functional change required,
but add a short TODO or upgrade note near the dependency declaration
(implementation 'org.jsoup:jsoup:1.17.2') to consider bumping to 1.21.2+ in the
future for improved XSS/ReDoS protections and note that current version includes
patches for CVE-2022-36033 and CVE-2021-37714.
In `@docs/Plan/02`. kpi_measurement_design.md:
- Around line 711-719: Update the fenced code block that begins with
"┌─────────────────────────────────┐" (the "Daily KPI Batch (매일 03:00)" ASCII
diagram) to include a language identifier on the opening fence (e.g., change ```
to ```text) so the Markdown linter recognizes the block language and enables
proper syntax highlighting; ensure only the opening fence is changed and the
closing ``` remains the same.
In `@frontend/.gitignore`:
- Line 34: 현재 .gitignore에 있는 `.env*` 규칙이 샘플 파일까지 무시하므로 `.env.example`가 제외되지 않도록
예외 규칙을 추가하세요: 수정 대상은 frontend/.gitignore의 기존 `.env*` 항목이며, 그 아래에 `.env.example`을
허용하는 부정 패턴을 추가하여 샘플 환경파일이 깃에서 관리되도록 만드세요 (즉, `.env*`는 유지하되 `.env.example`은 무시
대상에서 제외).
In `@frontend/package.json`:
- Line 19: package.json shows "next": "16.1.6" while the frontend architecture
doc states "Next.js 15 (App Router)"; either update the doc to reflect Next.js
16 or change the "next" field in package.json to the intended 15.x version.
Locate the "next" entry in package.json and the architecture documentation
mentioning "Next.js 15 (App Router)" and make them consistent, and if upgrading
to 16 keep any migration notes (App Router compatibility) and update
README/changelog accordingly.
In `@frontend/src/app/layout.tsx`:
- Line 32: The global body element in layout.tsx sets overflow-hidden which
blocks page scrolling; remove the overflow-hidden token from the body class
string in layout.tsx and instead apply scroll-lock only to the specific shell
container(s) that require it (for example the AppShell, Sidebar, or Header
wrapping element), or conditionally apply overflow-hidden when a
modal/side-drawer is open; update the class on the element(s) (e.g., AppShell or
the div that currently controls layout) to manage overflow and preserve h-screen
and transition classes on body.
In `@frontend/src/app/my-links/page.tsx`:
- Around line 105-139: Replace direct getState() calls with hooked actions:
instead of calling useUIStore.getState().toggleCreateFolderDialog(true) and
useUIStore.getState().toggleSaveLinkDialog(true) inline, pull the actions from
the hook at the top of the component (e.g., const toggleCreateFolderDialog =
useUIStore(s => s.toggleCreateFolderDialog) and const toggleSaveLinkDialog =
useUIStore(s => s.toggleSaveLinkDialog)) and call those functions in the button
onClick handlers; this keeps usage consistent with the existing hook pattern and
avoids mixing getState() with hook selectors.
In `@frontend/src/components/dialogs/CreateFolderDialog.tsx`:
- Line 104: The <label> elements inside the CreateFolderDialog component that
are not tied to form controls (e.g., the "Appearance" label at the reported
locations) should be changed to a semantic section header element such as <p> or
<span> so they aren't misinterpreted as control labels; update the two
occurrences (the "Appearance" label and the similar one around line 134) by
replacing the <label> tags with <p> (or <span>) while preserving the existing
classes (e.g., "text-sm font-semibold text-gray-700 dark:text-gray-200") and
surrounding structure so styling stays the same and accessibility semantics are
correct.
In `@frontend/src/components/dialogs/SaveLinkDialog.tsx`:
- Around line 155-156: Remove the client-supplied ownerMemberId (and
TEMP_MEMBER_ID usage) from the tag creation payload in SaveLinkDialog.tsx;
instead send only the tagName (e.g., { tagName: tagName.trim() }) and let the
server determine the owner from the authenticated request context. Update the
API call invocation (the function/method that receives this object) and any
related TypeScript types/interfaces to no longer require ownerMemberId so the
client cannot override ownership.
- Line 199: The review flags multiple standalone <label> elements in
SaveLinkDialog.tsx (e.g., the "URL" label at the label with className "block
text-[10px] font-extrabold text-slate-500 uppercase tracking-widest" and other
similar labels at the other noted positions) that are not associated with form
controls; fix by either replacing these decorative/section-title labels with
non-semantic elements (span or p) or by properly linking each label to its input
using htmlFor on the label and a matching id on the corresponding input (update
the relevant JSX for the URL, and the other occurrences at the noted positions
in SaveLinkDialog component). Ensure accessibility by choosing one of those two
approaches consistently across all listed occurrences.
- Around line 281-285: Replace the clickable divs used for folder tiles with
semantic buttons to restore keyboard accessibility: change the element that
currently uses key={folder.memberFolderId} onClick={() =>
setSelectedFolderId(isActive ? null : folder.memberFolderId)}
className={`${styles.folderTile} ${isActive ? styles.folderTileActive : ''}
group`} to a <button type="button"> while preserving the same key, onClick
handler, and className (and any children/attributes). Do the same replacement
for the other occurrences that render folder tiles (the blocks referenced around
the other ranges) so that all interactive folder items use button type="button"
instead of div.
- Around line 64-85: The effect calls analyzeUrlMutation.mutate on every url
change causing race conditions where slower responses overwrite newer titles;
modify the useEffect (and related hooks) to debounce the analyzeUrlMutation call
(e.g., 200–500ms) and ensure only the latest response updates state by tracking
request identity or comparing the response against the current url before
calling setDisplayTitle. Keep the immediate domain fallback
(setDisplayTitle(domain)) and the isTitleEdited guard, but replace the direct
analyzeUrlMutation.mutate call with a debounced invocation (use a timeout ref or
a requestId ref) and cancel/ignore previous pending responses so
analyzeUrlMutation.mutate and its onSuccess only setDisplayTitle when the
response matches the latest url/requestId.
In `@frontend/src/components/layout/Header.tsx`:
- Around line 19-23: In Header component replace the direct DOM toggle in the
onClick handler (document.documentElement.classList.toggle('dark')) with a
React-driven theme state: use a theme provider or next-themes' useTheme (or a
ThemeContext) to read/set theme and guard updates until after mount (e.g., check
mounted flag or useEffect) to avoid SSR hydration mismatches; update the
button's onClick to call the theme setter (e.g., setTheme or toggleTheme) and
remove any direct document.documentElement manipulation so theme state and UI
stay synchronized.
In `@frontend/src/components/my-links/RightPanel.tsx`:
- Line 47: Replace the loose any[] type on the RightPanel component prop
`folders` with the concrete interface/array type `FolderResponse[]` (or the
correct exported type name) so TypeScript can catch field-access mistakes;
update the prop/interface/type declaration that mentions `folders?: any[]` to
`folders?: FolderResponse[]` and import or declare `FolderResponse` where
`RightPanel` (or its props type) is defined.
- Around line 210-212: Validate and sanitize data.link.originalUrl before
calling window.open in RightPanel (where the current branch uses
window.open(data.link.originalUrl,...)); use the URL constructor to parse the
string and only allow protocols 'http:' or 'https:' (reject or no-op for other
schemes), and pass the validated URL string to window.open with the same target
and features ('_blank', 'noopener,noreferrer') to prevent opening malicious
schemes.
- Around line 877-879: The current loop calls deleteBookmarkMutation.mutate for
each id and immediately calls exitBulkMode(), which hides partial failures;
instead, perform asynchronous deletions and wait for results before exiting bulk
mode: use deleteBookmarkMutation.mutateAsync (or collect mutation promises) for
each id, await Promise.allSettled on those promises, count failures, and then
handle failures (show notification and/or trigger retries for failed ids) before
calling exitBulkMode(); reference selectedLinkIds,
deleteBookmarkMutation.mutateAsync (or mutate), and exitBulkMode() to locate and
update the code path.
- Around line 204-214: The clickable container div (referenced by itemRef in
RightPanel.tsx with the onClick that uses isBulkEditMode, onToggleSelect,
data.bookmarkId, isNoteEditing, isTitleEditing, data.link?.originalUrl) is not
keyboard-accessible; change this element to a semantic <button> (or if keeping a
div, add role="button", tabIndex={0} and an onKeyDown handler) so keyboard users
can activate it with Enter/Space. Ensure the onKeyDown mirrors the onClick logic
(call onToggleSelect for bulk mode, otherwise open originalUrl) and preserve
aria states if needed (e.g., aria-pressed or aria-selected) for selection; apply
the same change to the other instances called out (around lines noted: the other
two similar blocks). Ensure focus styles remain consistent and that
pointer/cursor behavior is unchanged.
- Around line 937-940: The onClick handler currently only closes the modal and
calls exitBulkMode but never invokes the move API; update the onClick of the
move confirmation button (the anonymous function where setIsMoveModalOpen(false)
and exitBulkMode() are called) to call the move function that performs the API
request (e.g., moveSelectedLinks, moveLinksToFolder, or performMove) with the
selected link IDs and target folder ID, await its result (or handle its
promise), only then close the modal and call exitBulkMode on success, and add
error handling to surface failures (e.g., set an error state or show a toast)
and refresh local state (or call fetchLinks/refreshLinks) after a successful
move so the UI reflects the change.
In `@frontend/src/components/ui/dialog.tsx`:
- Around line 50-82: DialogContent and DialogFooter both accept showCloseButton
causing duplicate close buttons; remove showCloseButton from DialogContent
(delete it from the prop signature and stop conditionally rendering
DialogPrimitive.Close in DialogContent) and let DialogFooter control the
close-button rendering via its showCloseButton prop (or, if you must keep both,
rename one prop and add a JSDoc comment on DialogFooter/DialogContent explaining
which component owns the close button); update references to showCloseButton in
DialogContent, DialogFooter, and any callers accordingly and add a short JSDoc
on the remaining prop to document expected usage.
In `@frontend/src/lib/api/bookmarkApi.ts`:
- Around line 74-79: The hook useBookmarks currently always fires
fetchBookmarks; add an enabled condition to the useQuery options so the query
only runs when search params are valid (e.g., when params.folderId is
defined/non-null or other required fields are present). Update the useQuery call
in useBookmarks to include an enabled flag derived from params (for example
based on params.folderId !== undefined && params.folderId !== null) while
keeping queryKey and queryFn (useBookmarks, params, fetchBookmarks) intact.
In `@frontend/src/lib/api/fetchClient.ts`:
- Around line 14-17: The current headers merge uses object spread and fails for
RequestInit.headers types like Headers or string[][]; update the headers
construction in fetchClient (where headers are set) to create a Headers instance
first (e.g., const headers = new Headers({'Content-Type': 'application/json'}),
then if options?.headers is a Headers iterate with forEach to copy entries, if
it's an array (string[][]) loop through pairs and set, and if it's a plain
object use Object.entries to set each key/value; finally assign that Headers
instance to the request init so all RequestInit.headers shapes are supported.
In `@frontend/src/lib/api/tagApi.ts`:
- Around line 47-49: Narrow the cache invalidation to the current user instead
of invalidating ['tags'] globally: update the onSuccess in the mutation to call
queryClient.invalidateQueries({ queryKey: ['tags', userId] }) (or the equivalent
user-scoped key your app uses) by passing the relevant userId (from the mutation
variables, auth store, or callback context) so queryClient.invalidateQueries and
the tags cache key become ['tags', userId] rather than just ['tags'].
- Around line 10-12: The client must not accept ownerMemberId from callers;
remove the ownerMemberId parameter from fetchTags and the other tag functions
(e.g., createTag and updateTag in this file) and call the server endpoint that
derives the owner from the authenticated session (e.g., GET /api/tags or GET
/api/tags/me and POST/PUT /api/tags) instead of embedding the owner in the
path/body; update fetchTags, createTag, and updateTag signatures to drop
ownerMemberId, change their fetchClient calls to the ownerless routes, and
update all local callers to stop passing ownerMemberId so ownership is enforced
by the server auth context.
In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`:
- Around line 233-234: In BookmarkServiceImpl replace the current silent
normalization that returns an empty string for blank URLs (the line `if (url ==
null || url.isBlank()) return "";`) with rejecting the request by throwing a
validation exception (e.g., IllegalArgumentException or a custom
BadRequestException) so an invalid/empty URL does not produce an abnormal link
key; update any callers or tests to expect/handle the thrown exception from the
method in BookmarkServiceImpl.
- Around line 334-343: In BookmarkServiceImpl replace the raw string
concatenation that builds oEmbedUrl so the url parameter is URL-encoded (use
URLEncoder.encode(url, StandardCharsets.UTF_8)) before appending to oEmbedUrl,
and after parsing the JSON title from root.path("title").asText() convert
empty/blank titles to null (e.g., if title == null or title.isBlank() return
null) so the upper-level fallback chain can run; update references to oEmbedUrl
and the title extraction logic accordingly.
- Around line 291-299: The extractTitle method in BookmarkServiceImpl currently
passes the raw URL into Jsoup.connect(), enabling SSRF; add strict URL
validation both at the controller entry and at the start of extractTitle: verify
the scheme is exactly http or https, parse the host and resolve it to an IP
(InetAddress) and reject if the hostname is localhost or 127.0.0.1 or
169.254.169.254 or maps to any private RFC1918 ranges (10.0.0.0/8,
172.16.0.0/12, 192.168.0.0/16) or IPv6 equivalents, and fail the request with a
clear error before calling Jsoup.connect(); keep the validation centralized
(e.g., a private validateUrlOrThrow(URL) helper used by the controller and
extractTitle) so Jsoup.connect(url) is only reached after the checks pass.
---
Outside diff comments:
In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`:
- Around line 217-224: The existence check uses the original url instead of the
normalized canonicalUrl, causing misses; update the logic in BookmarkServiceImpl
so after obtaining canonicalUrl (via normalizeUrl) and confirming a Link exists
(selectLinkByCanonicalUrl), you perform the bookmark existence check using the
canonical identifier (e.g., pass canonicalUrl to
bookmarkDao.checkBookmarkExistsByUrl or use the Link's id from
selectLinkByCanonicalUrl) instead of the raw url (memberId and url), so the
check is consistent with the canonicalization strategy.
---
Duplicate comments:
In `@docs/Backend/01`. backend-convention.md:
- Line 5: Add a single blank line immediately after the markdown heading "## 1)
네이밍 규칙" (and likewise after any other headings in this file that are directly
followed by content) to satisfy MD022; find occurrences where a heading is
followed on the next line by text or a list and insert one empty line between
the heading and the content so all headings have a blank line beneath them.
In `@docs/Fontend/01`. frontend_architecture.md:
- Line 42: Add a single blank line before and after each top-level/subheading to
satisfy markdownlint MD022: for example ensure there is one empty line above and
below "### 1. 프로젝트 기반 및 환경 설계" and do the same for the other affected headings
(the headings at the other flagged locations), so every heading has exactly one
blank line surrounding it; update the markdown accordingly and re-run lint to
confirm the MD022 warnings are resolved.
In `@docs/Plan/01`. searchweb_PRD.md:
- Around line 16-312: The document violates markdownlint rules MD022/MD058/MD047
due to missing blank lines around headers and tables and a missing final
newline; fix by ensuring there is exactly one empty line before and after each
header (e.g., "### 🗺️ MVP 우선순위 및 핵심 가치 (Roadmap)", "## 2. Problem &
Hypothesis", "## 3. Goal", etc.) and before/after every table block (the
pipe-style tables under Problem & Hypothesis, FR sections, etc.), remove any
extra multiple blank lines so only a single blank line surrounds those blocks,
and add a single newline character at the end of the file to ensure a final
newline.
In `@frontend/next.config.ts`:
- Around line 5-11: The rewrites() in next.config.ts currently hardcodes
'http://localhost:8080' causing failures in staging/production; change
rewrites() to build the destination from an environment variable (e.g.,
process.env.BACKEND_URL or NEXT_PUBLIC_BACKEND_URL) with a sensible localhost
default for local dev, and update any runtime config or .env docs so BACKEND_URL
is set in each environment; specifically modify the destination for source
'/api/:path*' to use that env var (keeping the '/api/:path*' suffix) so
deployments point to the correct backend URL.
In `@frontend/src/app/login/page.tsx`:
- Line 22: The anchor using href="#" for the "문의하기" link in page.tsx is a
placeholder causing accessibility/lint issues; replace it with a real navigation
or a button: either change the element to a proper Link/anchor with the real
path (e.g., "/contact") using your routing method, or convert the <a ...
href="#"> element with className "text-white/90 hover:text-white text-sm
font-medium transition-colors" and inner text "문의하기" into a <button
type="button"> and wire the click to navigate (e.g., router.push or onClick
handler). Apply the same change for the other duplicate occurrences (the other
"문의하기" anchors noted in the file).
In `@frontend/src/app/page.tsx`:
- Line 1: 현재 파일에 불필요한 "use client" 디렉티브가 있어 이 페이지는 서버 컴포넌트로 충분하므로 최상단의 "use
client" 문자열을 제거하세요; 제거 후 page.tsx에서
useState/useEffect/useRef/useMemo/useCallback 또는 onX 이벤트 핸들러 같은 클라이언트 전용 API가
사용되지 않는지 (예: 검색 정규식으로 검사한 결과) 확인하고, 만약 있다면 해당 로직을 클라이언트 컴포넌트로 분리하거나 제거하여 서버 컴포넌트
규약을 유지하도록 수정하세요.
- Around line 61-66: The SVG used as a decorative icon should be hidden from
assistive tech; update the <svg> element in frontend/src/app/page.tsx (the
inline Google-logo SVG) to include accessibility attributes such as
aria-hidden="true" and focusable="false" (and remove any title/desc that would
expose it) so screen readers ignore it; ensure you apply these attributes
directly on the svg tag that contains the four <path> children.
- Line 26: Several plain <button> elements in the page component are missing an
explicit type and can trigger unintended form submissions; update each button
element (e.g., the one with className "md:hidden text-white" and the other
similar buttons flagged in the review) to include type="button" so they do not
act as submit buttons inside form contexts, ensuring you add the attribute to
every <button> instance noted in the diff.
- Line 19: The anchor elements using href="#" (the <a className="text-white/90
hover:text-white text-sm font-medium transition-colors" href="#">문의하기</a> and
the duplicate occurrences) must be replaced with real targets or appropriate
controls: update each anchor's href to the actual route or external URL (e.g.,
"/contact" or "mailto:...") or, if it triggers a JS action rather than
navigation, replace with a button element or an accessible element with
role="button" and proper onClick handler; ensure to update the instances
referenced by the symbol of that anchor in this file and the duplicate
occurrences (lines noted in the review) so the links are navigable and
accessible.
In `@frontend/src/components/dialogs/CreateFolderDialog.tsx`:
- Around line 27-30: The UI state for selectedIcon, selectedColor, and isPinned
is not being sent to the backend; update the folder creation submit handler
(e.g., the function that dispatches createFolder / handleCreate / onCreateSubmit
in CreateFolderDialog.tsx) to include folderName, selectedIcon, selectedColor
and isPinned in the API payload, ensuring the same state variables
(selectedIcon, selectedColor, isPinned, folderName) are referenced; if the
backend API does not accept those fields, remove or hide the icon/color/pinned
controls from the dialog UI (or gate them behind a feature flag) so the UI
reflects available backend support.
- Around line 75-78: Multiple buttons inside CreateFolderDialog (e.g., the close
button with onClick={() => toggleCreateFolderDialog(false)} and other non-submit
buttons later in the component) are missing type="button", which can trigger
unwanted form submission; update every non-submit button in the
CreateFolderDialog component to include type="button" (for example add
type="button" to the button that calls toggleCreateFolderDialog(false) and to
the other action buttons in this component) so only the intended submit control
triggers form submit.
In `@frontend/src/components/layout/Sidebar.tsx`:
- Around line 82-89: The pinned folder button in Sidebar (the <button
key={folder.memberFolderId} onClick={() =>
setSelectedFolderId(folder.memberFolderId)} ...> element) is missing an explicit
type and should have type="button" added so clicking it does not trigger form
submission; update that button element in Sidebar.tsx to include type="button"
alongside its existing props.
- Around line 73-78: The button in Sidebar.tsx that calls
useUIStore.getState().toggleCreateFolderDialog(true) is missing an explicit type
and can accidentally submit a surrounding form; update the <button> element
rendering the "add" icon to include type="button" so it won't act as a submit
button when placed inside a form.
In `@frontend/src/lib/auth/currentUser.ts`:
- Line 3: TEMP_MEMBER_ID is a hardcoded fallback that can cause all requests to
be treated as the same user; update the exported TEMP_MEMBER_ID handling so that
in production it does not provide a fallback (throw or return null/error when
auth is missing) and only returns a safe explicit fallback when NODE_ENV ===
'development' or a dedicated DEV_FALLBACK flag is set; modify currentUser.ts to
replace the unconditional export with logic that reads process.env.NODE_ENV or a
DEV flag, returns the temporary id only in dev, and rejects/throws or returns
undefined in production so callers must handle missing authentication.
In `@frontend/src/lib/types/folder.ts`:
- Around line 14-16: The CreateFolderRequest interface exposes ownerMemberId
which allows clients to set the folder owner; remove ownerMemberId from the
CreateFolderRequest type definition and any client-side construction of that
payload (search for usages of CreateFolderRequest and ownerMemberId), update API
call sites to stop sending ownerMemberId, and ensure server-side folder creation
uses the authenticated user (not client-provided owner) to set the owner; also
update any tests/types that referenced ownerMemberId accordingly.
In `@frontend/src/lib/types/tag.ts`:
- Around line 9-11: The CreateTagRequest type exposes ownerMemberId which must
be removed so ownership is enforced server-side; delete the ownerMemberId field
from the CreateTagRequest interface and update all call sites that construct or
type-check CreateTagRequest (e.g., tag creation helpers, API client methods, and
any components or tests) to stop passing or expecting ownerMemberId, instead
rely on server/auth context to set the owner; ensure compile errors are fixed by
updating signatures like the createTag API call and related serializers/parsers
to reflect the new shape.
In `@src/main/resources/application.properties`:
- Line 13: Replace the hardcoded DEBUG setting in application.properties for
logging.level.com.web.SearchWeb with a non-DEBUG default (e.g., remove the key
or set to INFO) and move the DEBUG override into profile-specific files (e.g.,
application-dev.properties or application-local.properties) so that DEBUG is
only enabled in dev/local environments; update any documentation or README that
references logging setup to reflect the profile-based DEBUG configuration.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: ab3a5b04-98dd-4a67-8771-e763d58e4652
⛔ Files ignored due to path filters (7)
frontend/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlfrontend/public/file.svgis excluded by!**/*.svgfrontend/public/globe.svgis excluded by!**/*.svgfrontend/public/next.svgis excluded by!**/*.svgfrontend/public/vercel.svgis excluded by!**/*.svgfrontend/public/window.svgis excluded by!**/*.svgfrontend/src/app/favicon.icois excluded by!**/*.ico
📒 Files selected for processing (58)
.gitignorebuild.gradledocs/Backend/01. backend-convention.mddocs/Fontend/01. frontend_architecture.mddocs/Plan/01. searchweb_PRD.mddocs/Plan/02. kpi_measurement_design.mdfrontend/.gitignorefrontend/README.mdfrontend/components.jsonfrontend/eslint.config.mjsfrontend/next.config.tsfrontend/package.jsonfrontend/pnpm-workspace.yamlfrontend/postcss.config.mjsfrontend/src/app/globals.cssfrontend/src/app/layout.tsxfrontend/src/app/login/page.tsxfrontend/src/app/my-links/page.tsxfrontend/src/app/page.tsxfrontend/src/components/dialogs/CreateFolderDialog.tsxfrontend/src/components/dialogs/SaveLinkDialog.tsxfrontend/src/components/layout/AppLayout.tsxfrontend/src/components/layout/Header.tsxfrontend/src/components/layout/Sidebar.tsxfrontend/src/components/my-links/FolderCard.tsxfrontend/src/components/my-links/RightPanel.tsxfrontend/src/components/providers/QueryProvider.tsxfrontend/src/components/ui/badge.tsxfrontend/src/components/ui/button.tsxfrontend/src/components/ui/dialog.tsxfrontend/src/components/ui/input.tsxfrontend/src/components/ui/popover.tsxfrontend/src/components/ui/select.tsxfrontend/src/components/ui/separator.tsxfrontend/src/components/ui/switch.tsxfrontend/src/components/ui/textarea.tsxfrontend/src/lib/api/bookmarkApi.tsfrontend/src/lib/api/fetchClient.tsfrontend/src/lib/api/folderApi.tsfrontend/src/lib/api/tagApi.tsfrontend/src/lib/auth/currentUser.tsfrontend/src/lib/store/folderStore.tsfrontend/src/lib/store/linkStore.tsfrontend/src/lib/store/uiStore.tsfrontend/src/lib/types/apiResponse.tsfrontend/src/lib/types/bookmark.tsfrontend/src/lib/types/folder.tsfrontend/src/lib/types/tag.tsfrontend/src/lib/utils.tsfrontend/tsconfig.jsonsrc/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.javasrc/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.javasrc/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.javasrc/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.javasrc/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.javasrc/main/resources/application.propertiessrc/main/resources/db/init_postgres.sqlsrc/main/resources/mapper/bookmark-mapper.xml
| implementation 'org.springframework.boot:spring-boot-starter-validation' | ||
| implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' | ||
| implementation 'me.paulschwarz:spring-dotenv:4.0.0' | ||
| implementation 'org.jsoup:jsoup:1.17.2' |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🌐 Web query:
jsoup 1.17.2 security vulnerabilities CVE
💡 Result:
jsoup 1.17.2 (2023-12-29) is newer than the fixed releases for the two upstream jsoup CVEs commonly flagged by scanners, so it is not affected by them. [4]
Known jsoup CVEs (upstream):
- CVE-2022-36033 (XSS / sanitizer bypass) — affects jsoup when cleaning HTML if
SafeList.preserveRelativeLinksis enabled; patched in jsoup 1.15.3. Since 1.17.2 > 1.15.3, 1.17.2 includes the fix. [1] - CVE-2021-37714 (DoS in HTML/XML parsing) — affects versions prior to 1.14.2 when parsing untrusted input; patched in 1.14.2. Since 1.17.2 > 1.14.2, 1.17.2 includes the fix. [2]
Upstream’s own GitHub Security Advisories list only these two issues for jsoup. [3]
Sources: NVD CVE records and jsoup/GitHub advisories. [1][2][3][4]
🏁 Script executed:
# Search for jsoup dependency usage and BookmarkServiceImpl
rg -t java "jsoup" --max-count=20Repository: Searchweb-Dev/Searchweb-Back
Length of output: 276
🏁 Script executed:
# Find BookmarkServiceImpl file
fd -t f "BookmarkServiceImpl"Repository: Searchweb-Dev/Searchweb-Back
Length of output: 145
🏁 Script executed:
# Check for any jsoup version references in build files
rg "jsoup" build.gradle build.gradle.kts pom.xmlRepository: Searchweb-Dev/Searchweb-Back
Length of output: 247
🌐 Web query:
jsoup latest version 2024 2025 security updates
💡 Result:
Latest jsoup versions (2024–2025) + security-relevant notes
Latest releases by year
- 2025 (latest): jsoup 1.21.2 — 2025-08-25 [1]
(Other 2025 releases: 1.21.1 — 2025-06-23, 1.20.1 — 2025-04-29, 1.19.1 — 2025-03-04) [2] - 2024 (latest): jsoup 1.18.3 — 2024-12-02 [3]
(Also: 1.18.2 — 2024-11-27, 1.18.1 — 2024-07-10) [4]
Current latest overall (as of Mar 5, 2026)
- jsoup 1.22.1 — 2026-01-01 [5]
Security updates / vulnerabilities to be aware of
- CVE-2022-36033 (XSS in Cleaner/Safelist with
preserveRelativeLinks) is patched in jsoup 1.15.3; upgrade to ≥ 1.15.3 if you might be affected. [6] - Security-relevant hardening in newer releases (not necessarily tied to a CVE):
- 1.21.1 mentions “improved defense against mutation XSS attacks” via attribute escaping changes. [7]
- 1.22.1 adds optional re2j for linear-time regex selector evaluation (helps reduce ReDoS risk for user-supplied regex selectors) and adds a maximum parser depth limit to prevent unbounded stack growth. [5]
Practical recommendation: if you’re choosing in the 2024–2025 window, prefer 1.21.2; if you can take the newest, use 1.22.1. [1][5]
Sources:
[1] jsoup 1.21.2 release notes (2025-08-25) (jsoup.org)
[2] jsoup News (release list for 2024–2026) (jsoup.org)
[3] jsoup 1.18.3 release notes (2024-12-02) (jsoup.org)
[4] jsoup 1.18.2 release notes (2024-11-27) (jsoup.org)
[5] jsoup 1.22.1 release notes (2026-01-01) (jsoup.org)
[6] NVD entry for CVE-2022-36033 (patched in jsoup 1.15.3) (nvd.nist.gov)
[7] jsoup 1.21.1 release notes (mutation XSS defense note) (jsoup.org)
Citations:
- 1: https://jsoup.org/news/release-1.21.2
- 2: https://jsoup.org/news/
- 3: https://jsoup.org/news/release-1.18.3
- 4: https://jsoup.org/news/release-1.18.2
- 5: https://jsoup.org/news/release-1.22.1
- 6: https://nvd.nist.gov/vuln/detail/cve-2022-36033
- 7: https://jsoup.org/news/release-1.21.1
LGTM! Jsoup 의존성 추가 적절함
Jsoup 1.17.2는 HTML 파싱을 위해 안정적이고 널리 사용되는 라이브러리입니다. URL 제목 추출 기능에 적합합니다.
보안 검증 완료:
- CVE-2022-36033 (XSS)는 1.15.3에서 패치됨 — 1.17.2에 포함
- CVE-2021-37714 (DoS)는 1.14.2에서 패치됨 — 1.17.2에 포함
현재 버전은 안전하며, 필요하면 향후 1.21.2 이상으로 업그레이드 시 뮤테이션 XSS 공격 방어 개선, ReDoS 위험 완화 등 추가 보안 강화를 얻을 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@build.gradle` at line 37, The added dependency implementation
'org.jsoup:jsoup:1.17.2' is appropriate and safe for HTML title extraction; no
functional change required, but add a short TODO or upgrade note near the
dependency declaration (implementation 'org.jsoup:jsoup:1.17.2') to consider
bumping to 1.21.2+ in the future for improved XSS/ReDoS protections and note
that current version includes patches for CVE-2022-36033 and CVE-2021-37714.
| ``` | ||
| ┌─────────────────────────────────┐ | ||
| │ Daily KPI Batch (매일 03:00) │ | ||
| │ │ | ||
| │ 1. event_log 기반 KPI 쿼리 실행 │ | ||
| │ 2. kpi_daily_snapshot 갱신 │ | ||
| │ 3. 목표 미달 시 알림 (선택) │ | ||
| └─────────────────────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
코드 블록에 언어 식별자를 추가해 주세요.
마크다운 린터 경고에 따르면 Line 711의 코드 블록에 언어가 지정되지 않았습니다. 가독성과 구문 강조를 위해 언어를 명시하는 것이 좋습니다.
🔧 수정 제안
-```
+```text
┌─────────────────────────────────┐
│ Daily KPI Batch (매일 03:00) │📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| ┌─────────────────────────────────┐ | |
| │ Daily KPI Batch (매일 03:00) │ | |
| │ │ | |
| │ 1. event_log 기반 KPI 쿼리 실행 │ | |
| │ 2. kpi_daily_snapshot 갱신 │ | |
| │ 3. 목표 미달 시 알림 (선택) │ | |
| └─────────────────────────────────┘ | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 711-711: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/Plan/02`. kpi_measurement_design.md around lines 711 - 719, Update the
fenced code block that begins with "┌─────────────────────────────────┐" (the
"Daily KPI Batch (매일 03:00)" ASCII diagram) to include a language identifier on
the opening fence (e.g., change ``` to ```text) so the Markdown linter
recognizes the block language and enables proper syntax highlighting; ensure
only the opening fence is changed and the closing ``` remains the same.
| .pnpm-debug.log* | ||
|
|
||
| # env files (can opt-in for committing if needed) | ||
| .env* |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
.env.example가 함께 무시되지 않도록 예외 규칙을 추가해 주세요.
Line 34의 .env*는 샘플 환경파일까지 제외할 수 있어 온보딩/문서화에 불리합니다.
제안 변경사항
# env files (can opt-in for committing if needed)
.env*
+!.env.example📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .env* | |
| .env* | |
| !.env.example |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/.gitignore` at line 34, 현재 .gitignore에 있는 `.env*` 규칙이 샘플 파일까지 무시하므로
`.env.example`가 제외되지 않도록 예외 규칙을 추가하세요: 수정 대상은 frontend/.gitignore의 기존 `.env*`
항목이며, 그 아래에 `.env.example`을 허용하는 부정 패턴을 추가하여 샘플 환경파일이 깃에서 관리되도록 만드세요 (즉,
`.env*`는 유지하되 `.env.example`은 무시 대상에서 제외).
| "clsx": "^2.1.1", | ||
| "framer-motion": "^12.34.3", | ||
| "lucide-react": "^0.575.0", | ||
| "next": "16.1.6", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Next.js version 16 release 2026
💡 Result:
- Next.js 16 was released on October 22, 2025 (it’s already out; not a 2026 release). [1][2]
- As of March 5, 2026, the latest stable Next.js 16 version listed is v16.1.6 (Jan 27, 2026). [2][3]
- The Next.js changelog also shows ongoing v16 canary builds (e.g., v16.2.0-canary.80 on Mar 4, 2026). [3]
Sources: [1] Next.js blog post for Next.js 16, [2] endoflife.date Next.js page, [3] Next.js changelog.
🏁 Script executed:
# Search for PR objectives or description that mentions Next.js version
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "README*" \) | head -20Repository: Searchweb-Dev/Searchweb-Back
Length of output: 388
🏁 Script executed:
# Check if there's any PR description or objectives file
git log --oneline --all | head -20Repository: Searchweb-Dev/Searchweb-Back
Length of output: 989
🏁 Script executed:
# Search for any mention of Next.js 15, App Router, or version-related descriptions
rg -i "next\.?js.*15|app router" --type md --type txt --type jsonRepository: Searchweb-Dev/Searchweb-Back
Length of output: 415
🏁 Script executed:
# Check the pull request template
cat ./.github/pull_request_template.mdRepository: Searchweb-Dev/Searchweb-Back
Length of output: 216
🏁 Script executed:
# Search in all files for Next.js version references
rg "next.*15|version.*15|15.*app.router" -iRepository: Searchweb-Dev/Searchweb-Back
Length of output: 306
문서와 package.json의 Next.js 버전 불일치 확인
프론트엔드 아키텍처 문서에는 "Next.js 15 (App Router)"로 명시되어 있지만, package.json에는 "next": "16.1.6"이 설정되어 있습니다. Next.js 16으로의 의도된 업그레이드인 경우 문서를 업데이트하거나, 버전을 맞춰야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/package.json` at line 19, package.json shows "next": "16.1.6" while
the frontend architecture doc states "Next.js 15 (App Router)"; either update
the doc to reflect Next.js 16 or change the "next" field in package.json to the
intended 15.x version. Locate the "next" entry in package.json and the
architecture documentation mentioning "Next.js 15 (App Router)" and make them
consistent, and if upgrading to 16 keep any migration notes (App Router
compatibility) and update README/changelog accordingly.
| {/* Material Symbols Outlined Font */} | ||
| <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/> | ||
| </head> | ||
| <body className={`${inter.variable} font-sans antialiased bg-background-light dark:bg-background-dark text-text-main h-screen overflow-hidden flex transition-colors duration-200`}> |
There was a problem hiding this comment.
전역 body의 overflow-hidden은 공용 페이지 스크롤을 차단합니다.
로그인/랜딩 화면에서 작은 뷰포트 기준으로 콘텐츠 접근이 막힐 수 있습니다. 스크롤 제어는 앱 셸(사이드바/헤더 영역) 내부로 한정하는 편이 안전합니다.
🛠️ 제안 수정안
- <body className={`${inter.variable} font-sans antialiased bg-background-light dark:bg-background-dark text-text-main h-screen overflow-hidden flex transition-colors duration-200`}>
+ <body className={`${inter.variable} font-sans antialiased bg-background-light dark:bg-background-dark text-text-main min-h-screen flex transition-colors duration-200`}>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <body className={`${inter.variable} font-sans antialiased bg-background-light dark:bg-background-dark text-text-main h-screen overflow-hidden flex transition-colors duration-200`}> | |
| <body className={`${inter.variable} font-sans antialiased bg-background-light dark:bg-background-dark text-text-main min-h-screen flex transition-colors duration-200`}> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/layout.tsx` at line 32, The global body element in
layout.tsx sets overflow-hidden which blocks page scrolling; remove the
overflow-hidden token from the body class string in layout.tsx and instead apply
scroll-lock only to the specific shell container(s) that require it (for example
the AppShell, Sidebar, or Header wrapping element), or conditionally apply
overflow-hidden when a modal/side-drawer is open; update the class on the
element(s) (e.g., AppShell or the div that currently controls layout) to manage
overflow and preserve h-screen and transition classes on body.
| async function fetchTags(ownerMemberId: number): Promise<TagResponse[]> { | ||
| return fetchClient<TagResponse[]>(`/api/tags/owners/${ownerMemberId}`); | ||
| } |
There was a problem hiding this comment.
태그 조회/생성에서 소유자 식별자를 클라이언트 입력에 의존하면 안 됩니다.
ownerMemberId를 경로/바디로 외부에서 주입받는 구조는 사용자 간 데이터 접근 우회 위험이 있습니다. 소유자 식별은 서버 인증 컨텍스트에서 파생되도록 API를 정리하세요.
Also applies to: 18-22, 32-36
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/lib/api/tagApi.ts` around lines 10 - 12, The client must not
accept ownerMemberId from callers; remove the ownerMemberId parameter from
fetchTags and the other tag functions (e.g., createTag and updateTag in this
file) and call the server endpoint that derives the owner from the authenticated
session (e.g., GET /api/tags or GET /api/tags/me and POST/PUT /api/tags) instead
of embedding the owner in the path/body; update fetchTags, createTag, and
updateTag signatures to drop ownerMemberId, change their fetchClient calls to
the ownerless routes, and update all local callers to stop passing ownerMemberId
so ownership is enforced by the server auth context.
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: ['tags'] }); | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
태그 캐시 무효화 범위를 사용자 단위로 좁히는 것이 좋습니다.
현재는 ['tags'] 전체를 무효화해서 불필요한 재조회가 발생할 수 있습니다. 사용자 스코프 키(['tags', ...])로 맞춰 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/lib/api/tagApi.ts` around lines 47 - 49, Narrow the cache
invalidation to the current user instead of invalidating ['tags'] globally:
update the onSuccess in the mutation to call queryClient.invalidateQueries({
queryKey: ['tags', userId] }) (or the equivalent user-scoped key your app uses)
by passing the relevant userId (from the mutation variables, auth store, or
callback context) so queryClient.invalidateQueries and the tags cache key become
['tags', userId] rather than just ['tags'].
| if (url == null || url.isBlank()) return ""; | ||
| try { |
There was a problem hiding this comment.
빈 URL을 빈 문자열로 정규화하면 안 됩니다.
Line 233에서 ""를 반환하면 비정상 링크 키가 생성됩니다. 이 구간은 예외를 던져 요청 자체를 거절하는 것이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`
around lines 233 - 234, In BookmarkServiceImpl replace the current silent
normalization that returns an empty string for blank URLs (the line `if (url ==
null || url.isBlank()) return "";`) with rejecting the request by throwing a
validation exception (e.g., IllegalArgumentException or a custom
BadRequestException) so an invalid/empty URL does not produce an abnormal link
key; update any callers or tests to expect/handle the thrown exception from the
method in BookmarkServiceImpl.
| try { | ||
| Document doc = Jsoup.connect(url) | ||
| .timeout(5000) | ||
| .followRedirects(true) | ||
| .maxBodySize(512 * 1024) // 512KB만 읽기 (title은 <head>에 있으므로 충분) | ||
| .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") | ||
| .header("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7") | ||
| .referrer("https://www.google.com/") | ||
| .get(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# extractTitle 호출 지점 및 URL 검증 유무 확인
echo "=== extractTitle 호출 지점 및 URL 검증 검색 ==="
rg -n -C3 'extractTitle\(|analyze|/api/bookmarks/analyze|Jsoup\.connect\(' --type=java
echo ""
echo "=== URL/SSRF 방어 로직 탐색 ==="
rg -n -C3 'localhost|127\.0\.0\.1|169\.254\.169\.254|isPrivate|isLoopback|InetAddress|allowlist|whitelist|scheme' --type=javaRepository: Searchweb-Dev/Searchweb-Back
Length of output: 5798
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== extractTitle 메서드 전체 (279-330줄) ==="
sed -n '279,330p' src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java
echo ""
echo "=== isValidUrl 또는 validateUrl 메서드 검색 ==="
rg -n 'isValidUrl|validateUrl|validateBookmarkUrl' --type=java -A 10
echo ""
echo "=== 컨트롤러에서 URL 검증 로직 확인 ==="
sed -n '195,210p' src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java
echo ""
echo "=== 전체 validateUrl/validation 메서드 존재 확인 ==="
rg -n 'private.*validate|private.*isValid' --type=java -A 5Repository: Searchweb-Dev/Searchweb-Back
Length of output: 2362
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== BookmarkApiController analyzeUrl 메서드 전체 ==="
sed -n '199,215p' src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java
echo ""
echo "=== 모든 Java 파일에서 SSRF 또는 URL 검증 키워드 검색 ==="
rg -l 'SSRF|ssrf|블랙리스트|blacklist|화이트리스트|whitelist' --type=java
echo ""
echo "=== 전체 코드베이스에서 localhost/127 차단 로직 ==="
rg 'localhost|127\.0\.0\.1|10\.|192\.168|172\.1[6-9]|172\.2[0-9]|172\.3[01]' --type=java
echo ""
echo "=== extractYoutubeTitle 메서드 확인 (추가 검증 가능성) ==="
sed -n '332,350p' src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.javaRepository: Searchweb-Dev/Searchweb-Back
Length of output: 712
🏁 Script executed:
#!/bin/bash
set -eulo pipefail
echo "=== extractYoutubeTitle 메서드 전체 ==="
sed -n '332,350p' src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java
echo ""
echo "=== extractDomain 메서드 ==="
rg -n 'extractDomain' --type=java -A 8
echo ""
echo "=== URL 파라미터에 대한 `@Valid`, `@Validated` 애노테이션 검색 ==="
rg -n '@Valid|@Validated|@NotBlank|@Pattern.*url' --type=javaRepository: Searchweb-Dev/Searchweb-Back
Length of output: 5003
URL 제목 추출 경로에 SSRF 방어가 필수입니다.
사용자 입력 URL이 검증 없이 직접 Jsoup.connect()로 전달되고 있어 내부망/메타데이터 엔드포인트 접근이 가능합니다. 컨트롤러(203-204줄)에서 URL을 검증하지 않은 채로 서비스 메서드에 전달하며, extractTitle 메서드(279-326줄)에서도 요청 직전에 검증 로직이 없습니다.
다음을 구현하세요:
http://또는https://만 허용하는 스킴 검증- localhost, 127.0.0.1, 169.254.169.254 등 차단
- 사설대역(10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12) 차단
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`
around lines 291 - 299, The extractTitle method in BookmarkServiceImpl currently
passes the raw URL into Jsoup.connect(), enabling SSRF; add strict URL
validation both at the controller entry and at the start of extractTitle: verify
the scheme is exactly http or https, parse the host and resolve it to an IP
(InetAddress) and reject if the hostname is localhost or 127.0.0.1 or
169.254.169.254 or maps to any private RFC1918 ranges (10.0.0.0/8,
172.16.0.0/12, 192.168.0.0/16) or IPv6 equivalents, and fail the request with a
clear error before calling Jsoup.connect(); keep the validation centralized
(e.g., a private validateUrlOrThrow(URL) helper used by the controller and
extractTitle) so Jsoup.connect(url) is only reached after the checks pass.
| String oEmbedUrl = "https://www.youtube.com/oembed?url=" + url + "&format=json"; | ||
| Document doc = Jsoup.connect(oEmbedUrl) | ||
| .ignoreContentType(true) | ||
| .timeout(3000) | ||
| .get(); | ||
|
|
||
| String json = doc.text(); | ||
| ObjectMapper mapper = new ObjectMapper(); | ||
| JsonNode root = mapper.readTree(json); | ||
| return root.path("title").asText(); |
There was a problem hiding this comment.
YouTube oEmbed 요청 URL은 반드시 인코딩해야 합니다.
현재 문자열 결합 방식은 쿼리 파손을 유발할 수 있습니다. 또한 빈 title은 null로 취급해 상위 fallback 체인이 동작하게 해주세요.
권장 수정안
- String oEmbedUrl = "https://www.youtube.com/oembed?url=" + url + "&format=json";
+ String encoded = java.net.URLEncoder.encode(url, java.nio.charset.StandardCharsets.UTF_8);
+ String oEmbedUrl = "https://www.youtube.com/oembed?url=" + encoded + "&format=json";
@@
- return root.path("title").asText();
+ String title = root.path("title").asText(null);
+ return (title == null || title.isBlank()) ? null : title;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`
around lines 334 - 343, In BookmarkServiceImpl replace the raw string
concatenation that builds oEmbedUrl so the url parameter is URL-encoded (use
URLEncoder.encode(url, StandardCharsets.UTF_8)) before appending to oEmbedUrl,
and after parsing the JSON title from root.path("title").asText() convert
empty/blank titles to null (e.g., if title == null or title.isBlank() return
null) so the upper-level fallback chain can run; update references to oEmbedUrl
and the title extraction logic accordingly.
- headers 병합 시 객체 스프레드 대신 Headers API를 사용하여 모든 타입 명세(Headers, string[][], Record)를 안전하게 지원하도록 구조 개선
- 리플렉션을 통해 메서드 파라미터 이름을 유지하여 Spring Data JPA 및 MyBatis 등의 라이브러리와 호환성 확보
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fb547ac90
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| onClick={() => { | ||
| setIsMoveModalOpen(false); | ||
| exitBulkMode(); |
There was a problem hiding this comment.
Perform updates when applying bulk move
Selecting a destination folder in the “Move Links” modal only closes the modal and exits bulk mode; it never sends any update for selectedLinkIds, so users are told to move links but their bookmarks remain in the original folder. This is a functional regression in the bulk-edit flow because the action appears successful while no data is changed.
Useful? React with 👍 / 👎.
| if (path.length() > 1 && path.endsWith("/")) { | ||
| path = path.substring(0, path.length() - 1); | ||
| } |
There was a problem hiding this comment.
Normalize root trailing slash in canonical URL
The new canonicalization keeps / when the path length is 1, so https://example.com and https://example.com/ now normalize differently and are treated as separate links. Since link deduplication relies on canonical URL, this causes duplicate records and inconsistent bookmark-exists behavior for the same page depending on whether a trailing slash is present.
Useful? React with 👍 / 👎.
💡 이슈
resolve {#SW-56}
🤩 개요
🧑💻 작업 사항
1. 문서 및 환경 설정
2. 코어 비즈니스 로직 & API 연동 뼈대
3. 공통 UI 및 전역 레이아웃 컴포넌트
4. 핵심 도메인 뷰 (Pages) 구현
📖 참고 사항
Summary by CodeRabbit
Summary by CodeRabbit
새로운 기능
문서화
작업